We just start moving into React Hooks from the React life-cycle and one of the things that we noticed, is that there's no straightforward way to run something similar to ComponentWillMount for loading data before sending the rendered page to the user (since useEffect is not running on SSR).
Is there any easy way supported by React to do so?
I had the same problem. I've managed to solve it with a custom hook and by rendering application twice on the server-side. The whole process looks like this:
during the first render, collect all effects to the global context,
after first render wait for all effects to resolve,
render the application for the second time with all data.
I wrote an article with examples describing this approach.
This is a direct link to the example form article on CodeSandbox.
Also, I have published an NPM package that simplifies this process - useSSE on GitHub.
This is a little tricky but one way around is to do the fetch directly in the component as in
function LifeCycle(props){
console.log("perform the fetch here as in componentwillmount")
const [number, setNumber] = useState(0)
useEffect(() => {
console.log("componentDidMount");
return () => {
console.log("componentDidUnmount");
};
}, []);
}
Related
I understand a bit about the useEffect hook but I think there’s still more knowledge to grasp. Some of which are not in the documentation. Please any contribution will help a lot y’all.
Some of my questions are:
Does useEffect get called on initial render even in production just like in development?
If it does, how do we control this the best way?
How can we use a clean up function on this Hook?
How can we make asynchronous calls in useEffect?
My attempts on useEffect usually makes me feel like a bad developer
Please take a look at react docs and react beta docs.
It always runs when your component mounts, after the first render regardless of the environment. In development mode when strict mode is on, it runs twice:
When Strict Mode is on, React will run one extra development-only setup+cleanup cycle before the first real setup. This is a stress-test that ensures that your cleanup logic “mirrors” your setup logic and that it stops or undoes whatever the setup is doing. If this causes a problem, you need to implement the cleanup function.
I'm not really sure what you mean by controlling it the best way. Your effect or setup code runs whenever the component mounts. Maybe
How to handle the Effect firing twice in development? can help you. You sometimes might want to prevent the effect to be executed when the component mounts, you can skip the effect by using a ref. See this stackoverflow question
The function you return in the useEffect does the clean up for you. See. For instance if you add an event listener inside useEffect, you remove the listener inside the function you return inside of it. See this link
useEffect(() => {
const listener = () => { /* Do something */ };
window.addEventListener("click", listener);
return () => {
window.removeEventListener("click", listener);
};
}, []);
Yes you can. See this stackoverflow question and fetching data in docs
useEffect(() => {
async function asyncFunction() {
/* Do something */
}
asyncFunction();
}, []);
Update:
Take a look at You Might Not Need an Effect
. It explains some situations which you might not need an effect at all.
Removing unnecessary Effects will make your code easier to follow, faster to run, and less error-prone.
Update 2:
You can probably skip this part for now, but it might help you to have a better grasp of useEffect, event handlers and what to expect in the future.
Separating Events from Effects tries to explain the differences between effects and event handlers, why distinguishing between those two is important and using event handlers inside effects.
Event handlers only re-run when you perform the same interaction again. Unlike event handlers, Effects re-synchronize if some value they read, like a prop or a state variable, is different from what it was during the last render. Sometimes, you also want a mix of both behaviors: an Effect that re-runs in response to some values but not others. This page will teach you how to do that.
Sometimes, you might use an event handler which has access to the props or the state inside an effect. But you don't want the useEffect to be triggered every time the values used in the event handler change. The following example is taken form useEffect shouldn’t re-fire when event handlers change
.
function Chat({ selectedRoom }) {
const [muted, setMuted] = useState(false);
const theme = useContext(ThemeContext);
useEffect(() => {
const socket = createSocket('/chat/' + selectedRoom);
socket.on('connected', async () => {
await checkConnection(selectedRoom);
showToast(theme, 'Connected to ' + selectedRoom);
});
socket.on('message', (message) => {
showToast(theme, 'New message: ' + message);
if (!muted) {
playSound();
}
});
socket.connect();
return () => socket.dispose();
}, [selectedRoom, theme, muted]); // 🟡 Re-runs when any of them change
// ...
}
As you see, you do not want to reconnect every time theme or muted variables change. The only time you want the effect(connecting and disconnecting from the server) to run is when the selectedRoom value changes.
So the react team has proposed a RFC: useEvent which provides
A Hook to define an event handler with an always-stable function identity.
useEvent is an experimental and unstable API that has not yet been added to the React(stable versions) ye, so you can’t use it yet.
This might be off-topic but probably helps you to understand React and its lifecycles better: There is this issue useCallback() invalidates too often in practice issue on GitHub . One workaround would be to create a custom hook that returns a function that its identity is stable and won't change on re-renders:
function useEventCallback(fn) {
let ref = useRef();
useLayoutEffect(() => {
ref.current = fn;
});
return useCallback(() => (0, ref.current)(), []);
}
Or you could use the use-event-callback package.
Note that useEventCallback does not mimic useEvent precisely:
A high-fidelty polyfill for useEvent is not possible because there is no lifecycle or Hook in React that we can use to switch .current at the right timing. Although use-event-callback is “close enough” for many cases, it doesn't throw during rendering, and the timing isn’t quite right. We don’t recommend to broadly adopt this pattern until there is a version of React that includes a built-in useEvent implementation.
useEffect is a very powerful hook. Regarding your question:
useEffect(() => (), []) - this version without params will be called once on initial rendering
you can control useEffect with params [] and based on these params you can place some logic inside the callback function.
clean up function used before unmount of your component, it is a good place to remove listeners or close connection to resources like Databases, Camera and etc.
Example of async call
useEffect(() => {
// declare the data fetching function
const fetchData = async () => {
const data = await fetch('https://yourapi.com');
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);
}, [])
2021 UPDATE
Use a library that makes requests and cache them - react-query, swr, redux-toolkit-query
ORIGINAL QUESTION
I've been struggling with this for quite a long time and didn't find an answer.
I have a component that is the last step of some registration process during which I ask a user to enter its data through several forms. In this component, I send collected data to API. If the request is successful I show ok, if not I show error.
I have useEffect that sends the data. The function that performs this task lives in a context
const { sendDataToServer } = useContext(context)
useEffect(() => {
const sendData = async () => {
setLoading(true)
await sendDataToServer(...data)
setLoading(false)
}
sendData()
}, [sendDataToServer, data])
If I include sendDataToServer in the dependencies list this useEffect would go into an infinite loop, causing endless rerendering. I suppose this is because a reference to the function has a different value on every render.
I can of course redesign the app and do not keep the function in the context, but I do like it and don't consider it a bad practice (correct me if I am wrong)
So what are my options here? How do I keep the flow with the context API, but use useEffect with the correct list of dependencies?
You're right with your guess, that's why we got useCallback for referential equality.
You didn't post the sendDataToServer function, but it should look something like this with useCallback:
const sendDataToServer = useCallback(data => {
// your implementation
}, [your, dependencies])
After that you can safely use it in your useEffect.
I highly recommend Kent C. Dodd's blog posts: When to useMemo and useCallback
Smartassing now: If it's only purpose is sending data to the server (and not changing the app's state), I don't know why it should be part of the context. It could be a custom hook or even a static function.
Btw: There could be another problem: If the data dependency in your useEffect is changed when executing sendDataToServer, you will still have an endless loop (e. g. when you fetch the new data after executing sendDataToServer), but we can't see the rest of the code.
I am having trouble testing a drop down populated with data from an API call in React Testing Library. Below is a CodeSandbox showing the issue
https://codesandbox.io/s/mutable-sea-wtt9u
If I change App to use a hardcoded array to populate the drop down (commented out in App component), the test passes.
Thanks
When your data comes from an asynchronous fetch call, the DOM doesn't get updated synchronously, and you have to use one of the async utilities to wait for the update. This works in your case (tested in your Codesandbox):
// import `wait` directly from React Testing Library
import { render, wait } from '#testing-library/react';
...
await wait(() => {
fireEvent.change(selectElement, { target: { value: "1" } });
expect(selectElement.value).toBe("1");
});
Here's the React Testing Library docs on async utilities: https://testing-library.com/docs/dom-testing-library/api-async
EDIT: It looks like you might have changed your CodeSandbox code. Now you need to wait for the async call to be made before firing the event, since you're fetching data on mount. I've updated my answer and made sure tests pass on your current CodeSandbox.
You need to mock your fetch events. I wrote an article on how to do that. You can find it here.
My current server-side node server uses very similar setup to redux-saga real-word example.
It uses renderToString twice, which seems different from another example.
real-world server.js
const rootComp = <Root store={store} routes={routes} history={createMemoryHistory()} renderProps={renderProps} type="server"/>
store.runSaga(rootSaga).toPromise().then(() => {
console.log('sagas complete')
res.status(200).send(
layout(
renderToString(rootComp),
JSON.stringify(store.getState())
)
)
}).catch((e) => {
console.log(e.message)
res.status(500).send(e.message)
})
renderToString(rootComp)
store.close()
Another example just uses renderToString once. Can you guys explain why it needs to do renderToString twice with redux-saga ??
The first call (the one at the bottom) will render your App. It will dispatch every of your components actions and then use Saga to fetch / store datas.
Then you're calling store.close() that will stopping Saga to do more things (if you not stopping Saga, it's gonna wait forever because of the while(true) of Saga).
The second call (inside the promise) will render your app with the data that is stored inside your store. No more actions will be treated by Saga as you stopped it.
There is examples with only one "renderToString" because. Choose one :
there is no data to load, so no need to render twice
data are loaded by another way
I have the following code in my render method:
render() {
return (
<div>
{this.props.spatulaReady.ready() ? <p>{this.props.spatula.name}</p> : <p>loading spatula</p>}
</div>
)
}
Which according to my understanding, checks if the subscriptionhandle is ready (data is there) and displays it. If no data is available, it should display a simple loading message. However, when I first load the page this snippet is on, it get's stuck on the loading part. On a page reload the data (usually) displays fine.
If I check the spatulaReady.ready() when the page first loads and while the display is stuck on 'loading spatula', and the data that should be there, the handle reports as ready and the data is there like it is supposed to be. If I refresh the page it all displays fine as well. The problem is, this way of checking for data and rendering if it has arrived has worked fine for me in the past. Is it because the render method is not reactive? Because handle.ready() should be reactive.
What makes it even weirder is that it sometimes DOES correctly display the data on page load, seemingly at random.
CreateContainer code:
export default createContainer(props => {
return {
user: Meteor.user(),
spatulaReady: Meteor.subscribe('spatula.byId', props.deviceId),
spatula: SpatulaCollection.findOne()
}
}, SpatulaConfig)
Publication code:
Meteor.publish('spatula.byId', function(deviceId) {
const ERROR_MESSAGE = 'Error while obtaining spatula by id'
if (!this.userId) //Check for login
throw new Meteor.Error('Subscription denied!')
const spatula = SpatulaCollection.findOne({_id: deviceId})
if(!spatula) //No spatula by this id
throw new Meteor.Error(403, ERROR_MESSAGE)
if(spatula.ownedBy != this.userId) //Spatula does not belong to this user
throw new Meteor.Error(403, ERROR_MESSAGE)
return SpatulaCollection.find({_id: deviceId})
})
I know I'm missing a piece of the puzzle here, but I've been unsuccessful at finding it. If you don't know the solution to my specific problem, pointing me in the right direction with another way of waiting for the data to arrive before displaying it is also greatly appreciated.
EDIT: After doing some trial-and-error and reading various other posts somewhat related to my project, I figured out the solution:
export default createContainer(props => {
const sHandle= Meteor.subscribe('spatula.byId', props.deviceId)
return {
user: Meteor.user(),
spatulaReady: sHandle.ready(),
spatula: SpatulaCollection.findOne()
}
}, SpatulaConfig)
It still makes no sense to me that moving the ready() call to create container fixed all my problems though.
As you figured out, moving the .ready() call to createContainer fixes the problem. This is because Meteor reactivity only works when you call a reactive data source (a reactive function), such as collection.find() or subscriptionHandle.ready() within a reactive context, such as Tracker.autorun or createContainer. Functions within the React component, including render, are not reactive contexts from Meteor's perspective.
Note that React and Meteor reactivity are two different things. React's reactivity works simply so that whenever a component's props or state change, it's render function is re-run. It does not understand anything about Meteor's reactive data sources. Since createContainer (that is re-run by Meteor reactivity when reactive data sources in it change) simply passes props to the underlying component, the component is re-rendered by React when the props given from createContainer change.