I'm working on a web app with NextJS, Apollo and React (hooks).
I have a form that asks the name of the visitor as the first step in a registration process.
When submitting the form the name will be saved in the Apollo cache and the visitor gets redirected to the next page.
import React, { useState } from 'react';
import Router , {useRouter} from 'next/router';
import { useApolloClient } from '#apollo/react-hooks';
const NameForm = props => {
const [name, setName] = useState("");
const client = useApolloClient();
const router = useRouter();
const handleSubmit = e => {
e.preventDefault();
if(!name) return;
client.writeData({ data: { name } });
router.push('/user/register');
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Naam</label>
<div>
<input type="text" id="name" name="name" value={name} onChange={e => setName(e.target.value)} />
<button type="submit" onClick={handleSubmit}>Get started</button>
</div>
</div>
</form>
)
}
export default NameForm;
The next page contains a more extensive form. When visitors come from the homepage, the name is already known and I want to get it back from the cache. I thought
import { gql } from 'apollo-boost';
import { useApolloClient } from '#apollo/react-hooks';
import AddUserForm from '../../components/forms/AddUserForm';
const GET_NAME = gql`
query GetName {
name #client
}`;
const AddUser = ({ name }) => (
<React.Fragment>
<AddUserForm name={name} />
</React.Fragment>
)
AddUser.getInitialProps = async ctx => {
const client = useApolloClient();
const name = await client.cache.readQuery({ query: GET_NAME });
return { name: name || '' };
}
export default AddUser;
I thought I could do this in the getInititialProps hooks are only allowed in the body of a functional component.
Because of the continuous development of next, react hooks and apollo I'm missing a tutorial/course about this and I find it difficult to find a right way to do this.
I hope someone here can help me further.
use apollo-client cache can lead you to some questions that really depends on the apollo-client's implementation and nextjs implementation.
If you open your app by entering the url to the browser address bar, Next.js will make requests (assuming the view need to fetch data) from server-side, then send to the client the rendered HTML.
Because apollo-client fetch then cache the data from server side, then the question is "Does Next.js send the apollo-client with its cache to client side for next request?"
You cannot sure about this unless you understand clearly about Next.js and apollo-client cache (about its implementation or how it works inside, if apollo cache data in-memory on server-side, you will fail if you go this way)
The answer is unsure because it depends on two stuffs at the same time. And maybe changed on the future!
So to deal with this problem, just use the Next.js way, it has designed a tunnel for data, it is the query on the url.
const NameForm = props => {
const [name, setName] = useState("");
const client = useApolloClient();
const router = useRouter();
const handleSubmit = e => {
e.preventDefault();
if(!name) return;
router.push(`/user/register?name=${name}`);
}
//render ...
}
import { useRouter } from 'next/router';
import AddUserForm from '../../components/forms/AddUserForm';
const AddUser = () => {
const router = useRouter();
return (
<React.Fragment>
<AddUserForm name={router.query.name} />
</React.Fragment>
)
}
export default AddUser;
If you want to send an object instead of a string?
const data = { name: "FoxeyeRinx", email: "foxeye.rinx#gmail.com" };
const base64 = btoa(JSON.stringify(data));
router.push(`/user/register?data=${base64}`);
const AddUser = () => {
const router = useRouter();
const base64 = router.query.data;
//decode base64 then parse it to js object
const data = JSON.parse(atob(base64));
return (
<React.Fragment>
<AddUserForm data={data}/>
</React.Fragment>
)
}
If you think the query is ugly and want to hide the query, use this guide: https://nextjs.org/learn/basics/clean-urls-with-dynamic-routing
Related
I have a mock database for playing around with some loaders and actions.
Here's the rough layout:
const db = { key: "bar" }
export const action = async ({ request }) => {
db.key = "foo"
}
export const loader = async ({ request }) => {
return json(db)
}
I have an issue though. When the action is called, it successfully updates db.key, however, the loader is called afterwards & the value is {key: "bar" }. Does anyone know why the object is not updated when the loader is called again?
I think that there is something related to "Data Writes" topic.
Whenever a form is submitted it reload all of the data for all of the routes on the page. Thats why many people reach for global state management libraries like redux.
I don't know if it helps but here I got a simple way where you can send a post request, update it with the action and load the content searching at the URL with the loader.
export const action: ActionFunction = async ({ request }) => {
const form = await request.formData();
const key = form.get("key");
return redirect(`/?key=${key}_updated`);
};
export const loader: LoaderFunction = async ({ request }: any) => {
const url = new URL(request.url);
const key = url.searchParams.get("key");
return json({ key });
};
export default function Index() {
const loaderData = useLoaderData();
return (
<div>
<span>loaderData: {loaderData.key}</span>
<Form method="post">
<input type="text" id="key" name="key" defaultValue={loaderData.key} />
<button>Submit</button>
</Form>
</div>
);
}
import React, {useState, useEffect, useContext} from 'react'
import AuthContext from '../context/AuthContext'
const HomePage = () => {
const [note,setNote] = useState([])
let {authTokens} = useContext(AuthContext)
useEffect(()=>{
getNotes()
},[])
let getNotes = async () => {
let response = await fetch("http://127.0.0.1:8000/api/notes",{
method:"GET",
headers:{
"Content-type":"application/json",
"Authorization":"Bearer "+String(authTokens.access)
}
})
let data =await response.json();
console.log("data i have been wait",data)
console.log("Data in it",note)
}
return (
<div>
<p>You are logged in to home paage</p>
{/* {notes.map(note =>{
<li >{note.body}</li>
})} */}
</div>
)
}
export default HomePage
I got my data from django backend and try to show in react using map but it doesnt map.
I can get the data but when I am trying to setNote(data) it doesn't not work.
Sometimes it works but still cant map it. I cant refresh map func and put data on it
and I don't get any error.[console.log like that][1]
console.log("data i have been wait",data)
setNote(data)
console.log("Data in it",note)```
[1]: https://i.stack.imgur.com/h17QF.png
In the case the setState works correctly but you can't map the data, I suggest that the problem comes from the fact that you put brackets inside your map but no return.
Try this instead:
{notes.map(note =>
{
return(<li >{note.body}</li>);
}
)}
or with arrow function:
{notes.map(note => <li>{note.body}</li>)}
I need a solution or at least a tip for my problem in which I have been stuck quite a long time. So I want to retrieve an image which is coming from the backend through an http request(2 Images , 2 requests). I have used .map() for the text data to be displayed , but how can I use it for inserting the relevant images?
(And I don't think the image can be stored in an array as I have done below)
The 2 images should be in CardImg component and the other should be sent as a prop to another component.
The images are obtained by ID of the collection. Below is the frontend code.
import {Card , CardImg , CardBody , CardTitle , CardText , Button } from 'reactstrap';
import {useState} from 'react';
import { useEffect } from 'react';
import ItineraryContainer from 'components/ItineraryContainer';
import Backdrop from 'components/ItineraryBackdrop';
import styles from '../assets/css/Itinerary.module.css'
import axios from 'axios';
function Itineraries(){
const [itineraries , setItineraries] = useState([]);
const [itineraryImage , setItineraryImage] = useState([]);
const [itineraryCovImage , setCovImage] = useState ([]);
useEffect(() =>{
axios.get("http://localhost:8070/itineraries/").then((res)=>{
setItineraries(res.data);
console.log(res.data);
itineraries.map((i) =>{
const id = i._id;
axios.get(`http://localhost:8070/itineraries/getImage/${id}`).then((r)=>{
itineraryImage.push(r.data);
}).catch((err) =>{
console.log(err);
})
axios.get(`http://localhost:8070/itineraries/getCovImage/${id}`).then((r) =>{
itineraryCovImage.push(r.data);
}).catch((err) =>{
console.log(err);
})
})
}).catch((err)=>{
console.log(err);
})
} , []);
const [ItineraryisOpen , setItineraryOpen] = useState(false);
function ViewItinerary(){
setItineraryOpen(true);
}
function ItineraryisClosed(){
setItineraryOpen(false);
}
return(
<div>
<div className = {styles.Packages}>
<h3>Our Tours</h3>
<br/><br/>
<div className = {styles.container} >
{itineraries.map((itinerary) =>(
<Card style = {{width: '20rem' , margin : '50px'}}>
<CardImg top src = "img.jpg" alt = "TourImage" />
<CardBody>
<CardTitle>{itinerary.itineraryName}</CardTitle>
<CardText>
{itinerary.itineraryDesc}<br/><br/>
<b>Itinerary Days : {itinerary.itineraryDays} </b>
<b>Itinerary Class : {itinerary.itineraryClass}</b>
<b>Price Per Adult : {itinerary.itineraryPriceAdult} </b> <br/>
<b>Price per Child : {itinerary.itineraryPriceChild} </b>
</CardText>
<Button color = "primary" onClick = {ViewItinerary} >View Itinerary</Button>
<Button color = "info" style = {{float : 'right'}}>Book Tour</Button>
</CardBody>
</Card>
))}
</div>
{ItineraryisOpen ? <ItineraryContainer onCancel = {ItineraryisClosed}/> : null}
{ItineraryisOpen && <Backdrop onCancel = {ItineraryisClosed}/>}
</div>
</div>
);
}
export default Itineraries;
Below is the code for the routes
//Fetch one Itinerary Image
router.route("/getImage/:id").get(async(req,res)=>{
const itinerary = req.params.id;
const itin = await Itinerary.findById(itinerary).then((data)=>{
const image = data.itineraryImage;
const file = `./images/${image}`;
res.download(file);
}).catch((err)=>{
res.status(500).send({status : "Fetching Image unsuccesful!"});
})
})
//Fetch one Itinerary Cover Image
router.route("/getCovImage/:id").get(async(req,res)=>{
const itinerary = req.params.id;
const itin = await Itinerary.findById(itinerary).then((data)=>{
const image = data.itineraryCoverImage;
const file = `./images/${image}`;
res.download(file);
}).catch((err)=>{
res.status(500).send({status : "Fetching Image unsuccesful!"});
})
})
(The backend gives the image as response , checked using postman).
Thank you!
Turns out that I have made things complicated. I directly used the HTTP request for the src attribute and it works! (Removed all the code inside the useEffect except for the "itinerary")
This did the job.
<CardImg top src = {`http://localhost:8070/itineraries/getImage/${itinerary._id}`}
I have Apollo Client running on my React app, and trying to keep authentication info in a Reactive Variable using useReactiveVar. Everything works in the dummy function when I first set the variable, however it resets the state after refreshing the app.
Here's my cache.js:
import { InMemoryCache, makeVar } from "#apollo/client";
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
isLoggedIn: {
read() {
return isLoggedInVar();
},
},
},
},
},
});
export const isLoggedInVar = makeVar();
export default cache;
Here's the component that reads the variable and renders different elements based on its state:
import React from "react";
import { useReactiveVar, useMutation } from "#apollo/client";
import MainButton from "../common/MainButton";
import { isLoggedInVar, userAddressVar } from "../../cache";
import { CREATE_OR_GET_USER } from "../../mutations/User";
const Profile = () => {
const isLoggedIn = useReactiveVar(isLoggedInVar);
const [createOrGetUser] = useMutation(CREATE_OR_GET_USER);
const handleCreateOrGetUser = () => {
const loginInput = {
address: 'text',
};
createOrGetUser({
variables: {
loginInput: loginInput,
},
}).then((res) => {
isLoggedInVar(true);
});
};
const profileComponent = isLoggedIn ? (
<div>Logged In</div>
) : (
<div onClick={handleCreateOrGetUser} className="profile-image"></div>
);
return (
<div className="profile-container">
{profileComponent}
</div>
);
};
export default Profile;
This component gets re-rendered properly when I invoke handleCreateOrGetUser, however, when I refresh the page, it resets the isLoggedInVar variable.
What would be the proper way to use Reactive Variables here to persist the cache?
It's not currently achievable using Apollo API according to their documentation.
There is currently no built-in API for persisting reactive variables,
but you can write variable values to localStorage (or another store)
whenever they're modified, and initialize those variables with their
stored value (if any) on app load.
There is a PR for that. https://github.com/apollographql/apollo-client/pull/7148
My application has a lot of redux-form. I am using Jest and Enzyme for unit testing. However, I fail to test the redux-form. My component is a login form like:
import { login } from './actions';
export class LoginForm extends React.Component<any, any> {
onSubmit(values) {
this.props.login(values, this.props.redirectUrl);
}
render() {
const { handleSubmit, status, invalid } = this.props;
return (
<form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
<TextField label="Email" name="email">
<TextField type="password" label="Password" name="password" autoComplete/>
<Button submit disabled={invalid} loading={status.loading}>
OK
</Button>
</form>
);
}
}
const mapStateToProps = (state) => ({
status: state.login.status,
});
const mapDispatchToProps = { login };
const form = reduxForm({ form: 'login' })(LoginForm);
export default connect(mapStateToProps, mapDispatchToProps)(form);
Mock the store, Import connected component
redux-form uses the store to maintain the form inputs. I then use redux-mock-store:
import ConnectedLoginForm from './LoginForm';
const configureStore = require('redux-mock-store');
const store = mockStore({});
const spy = jest.fn();
const wrapper = shallow(
<Provider store={store}>
<ConnectedLoginForm login={spy}/>
</Provider>);
wrapper.simulate('submit');
expect(spy).toBeCalledWith();
But in this way, the submit is not simulated, my test case failed:
Expected mock function to have been called with: []
But it was not called.
Mock the store, Import React component only.
I tried to create redux form from the testing code:
import { Provider } from 'react-redux';
import ConnectedLoginForm, { LoginForm } from './LoginForm';
const props = {
status: new Status(),
login: spy,
};
const ConnectedForm = reduxForm({
form: 'login',
initialValues: {
email: 'test#test.com',
password: '000000',
},
})(LoginForm);
const wrapper = shallow(
<Provider store={store}>
<ConnectedForm {...props}/>
</Provider>);
console.log(wrapper.html());
wrapper.simulate('submit');
expect(spy).toBeCalledWith({
email: 'test#test.com',
password: '000000',
});
In this case, i still got error of function not called. If I add console.log(wrapper.html()), I got error:
Invariant Violation: Could not find "store" in either the context or
props of "Connect(ConnectedField)". Either wrap the root component in
a , or explicitly pass "store" as a prop to
"Connect(ConnectedField)".
I cannot find documentations on official sites of redux-form or redux or jest/enzyme, or even Google.. Please help, thanks.
I used the real store (as redux-mock-store does not support reducers) and redux-form's reducer, it worked for me. Code example:
import { createStore, Store, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import { reducer as formReducer } from 'redux-form';
const rootReducer = combineReducers({
form: formReducer,
});
let store;
describe('Redux Form', () => {
beforeEach(() => {
store = createStore(rootReducer);
});
it('should submit form with form data', () => {
const initialValues = {...};
const onSubmit = jest.fn();
const wrapper = mount(
<Provider store={store}>
<SomeForm
onSubmit={onSubmit}
initialValues={initialValues}
/>
</Provider>
);
const form = wrapper.find(`form`);
form.simulate('submit');
const expectedFormValue = {...};
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit.mock.calls[0][0]).toEqual(expectedFormValue);
});
});
You can find the answer here: https://github.com/tylercollier/redux-form-test
In short, you can use shallow dive() function to test higher-order component, but in your case, you have a higher-order component inside a higher-order component.
You need to break you component into two components, the first one is a presentation component, without
const form = reduxForm({ form: 'login' })(LoginForm);
export default connect(mapStateToProps, mapDispatchToProps)(form);
You then wrap the first component into the second component (container component).
You can easily test the first component (presentation component)
I had the similar problem. The answer can be found here https://github.com/airbnb/enzyme/issues/1002.
Long story short, you should pass store as a prop into your form and use .dive() function on the wrapper.
Regards
Pavel
I made a tool which helps with problems like that. It make a test-cases with real data (chrome extension collect it and save to file) which you can run by CLI tool.
I recommend you to try it: https://github.com/wasteCleaner/check-state-management