How to properly implement "subscription"-like fetches with React useEffect - reactjs

I have a question about the "proper" (or most idiomatic) way to implement network fetch behavior in React based on a single changing property.
A simplified example of the functionality I'm building is below: I am looking to build a multi-page form that "auto-saves" a draft of form inputs as the user navigates back/forth between pages.
TL;DR - I thought useEffect hooks would be the right way to save a draft to the backend every time a url slug prop changes, but I'm running into issues, and wondering about suggestions for the "right" tool for this type of behavior.
Here is my attempt so far. My code is technically working how I want it to, but violates React's recommended hook dependency pattern (and breaks the exhaustive-deps ESLint rule).
import React from 'react';
const ALL_SLUGS = [
'apple',
'banana',
'coconut',
];
function randomUrlSlug() {
return ALL_SLUGS[Math.floor((Math.random() * ALL_SLUGS.length))];
}
// just resovles the same object passed in
const dummySaveDraftToBackend = (input) => {
return new Promise((resolve, _reject) => {
setTimeout(() => {
resolve(input);
}, 1000);
});
};
export function App() {
const [urlSlug, setUrlSlug] = React.useState(randomUrlSlug());
return (
<MyComponent urlSlug={urlSlug} setUrlSlug={setUrlSlug} />
);
}
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
// useCallback memoization is technically unnecessary as written here,
// but if i follow the linter's advice (listing handleSave as a dependency of the useEffect below), it also suggests memoizing here.
// However, complexState is also technically a dependency of this callback memo, which causes the fetch to trigger every time state changes.
//
// Similarly, moving all of this inside the effect hook, makes the hook dependent on `complexState`, which means the call to the backend happens every time a user changes input data.
const handleSave = React.useCallback(() => {
console.log('*** : start fetch');
setLoading(true);
dummySaveDraftToBackend(complexState).then((resp) => {
console.log('fetch response: ', resp);
// to keep this example simple, here we are just updating
// a dummy "responseCount", but in the actual implementation,
// I'm using a state reducer, and want to make some updates to form state based on error handling, backend validation, etc.
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState]);
// I know this triggers on mount and am aware of strategies to prevent that.
// Just leaving that behavior as-is for the simplified example.
React.useEffect(() => {
if (urlSlug) {
handleSave();
}
}, [urlSlug]); // <- React wants me to also include my memoized handleSave function here, whose reference changes every time state changes. If I include it, the fetch fires every time state changes.
return (
<div className="App">
<h2>the current slug is:</h2>
<h3>{urlSlug}</h3>
<div>the current state is:</div>
<pre>{JSON.stringify(complexState, null, 2)}</pre>
<div>
<h2>edit foo</h2>
<input value={complexState.foo} onChange={(e) => setComplexState((s) => ({ ...s, foo: e.target.value }))} disabled={loading} />
</div>
<div>
<h2>edit baz</h2>
<input value={complexState.baz} onChange={(e) => setComplexState((s) => ({ ...s, baz: e.target.value }))} disabled={loading} />
</div>
<div>
<button
type="button"
onClick={() => setUrlSlug(randomUrlSlug())}
disabled={loading}
>
click to change to a random URL slug
</button>
</div>
</div>
);
}
As written, this does what I want it to do, but I had to omit my handleSave function as a dependency of my useEffect to get it to work. If I list handleSave as a dependency, the hook then relies on complexState, which changes (and thus fires the effect) every time the user modifies input.
I'm concerned about violating React's guidance for not including dependencies. As-is, I would also need to manually prevent the effect from running on mount. But because of the warning, I'm wondering if I should not use a useEffect pattern for this, and if there's a better way.
I believe I could also manually read/write state to a ref to accomplish this, but haven't explored that in much depth yet. I have also explored using event listeners on browser popstate events, which is leading me down another rabbit hole of bugginess.
I know that useEffect hooks are typically intended to be used for side effects based on event behavior (e.g. trigger a fetch on a button click). In my use case however, I can't rely solely on user interactions with elements on the page, since I also want to trigger autosave behavior when the user navigates with their browser back/forward controls (I'm using react-router; current version of react-router has hooks for this behavior, but I'm unfortunately locked in to an old version for the project I'm working on).
Through this process, I realized my understanding might be a bit off on proper usage of hook dependencies, and would love some clarity on what the pitfalls of this current implementation could be. Specifically:
In my snippet above, could somebody clarify to me why ignoring the ESLint rule could be "bad"? Specifically, why might ignoring a dependency on some complex state can be problematic, especially since I dont want to trigger an effect when that state changes?
Is there a better pattern I could use here - instead of relying on a useEffect hook - that is more idiomatic? I basically want to implement a subscriber pattern, i.e. "do something every time a prop changes, and ONLY when that prop changes"

If all the "state" that is updated after saving it to backend is only a call count, declare this as a separate chunk of state. This eliminates creating a render loop on complexState.
Use a React ref to cache the current state value and reference the ref in the useEffect callback. This is to separate the concerns of updating the local form state from the action of saving it in the backend on a different schedule.
Ideally each useState hook's "state" should be closely related properties/values. The complexState appears to be your form data that is being saved in the backend while the responseCount is completely unrelated to the actual form data, but rather it is related to how many times the data has been synchronized.
Example:
export function MyComponent({ urlSlug, setUrlSlug }) {
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow' });
const [responseCount, setResponseCount] = React.useState(0);
const complexStateRef = React.useRef();
React.useEffect(() => {
complexStateRef.current = complexState;
}, [complexState]);
React.useEffect(() => {
const handleSave = async (complexState) => {
console.log('*** : start fetch');
setLoading(true);
try {
const resp = await dummySaveDraftToBackend(complexState);
console.log('fetch response: ', resp);
setResponseCount(count => count + 1);
} catch(error) {
// handle any rejected Promises, errors, etc...
} finally {
setLoading(false);
}
};
if (urlSlug) {
handleSave(complexStateRef.current);
}
}, [urlSlug]);
return (
...
);
}

This feels like a move in the wrong direction (towards more complexity), but introducing an additional state to determine if the urlSlug has changed seems to work.
export function MyComponent({ urlSlug, setUrlSlug }) {
const [slug, setSlug] = React.useState(urlSlug);
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
const handleSave = React.useCallback(() => {
if (urlSlug === slug) return // only when slug changes and not on mount
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}, [complexState, urlSlug, slug]);
React.useEffect(() => {
if (urlSlug) {
handleSave();
setSlug(urlSlug)
}
}, [urlSlug, handleSave]);
Or move handleSave inside the useEffect (with additional slug check)
Updated with better semantics
export function MyComponent({ urlSlug, setUrlSlug }) {
const [autoSave, setAutoSave] = React.useState(false); // false for not on mount
React.useEffect(() => {
setAutoSave(true)
}, [urlSlug])
const [loading, setLoading] = React.useState(false);
const [complexState, setComplexState] = React.useState({ foo: 'bar', baz: 'wow', responseCount: 0 });
React.useEffect(() => {
const handleSave = () => {
if(!autoSave) return
console.log('*** : start fetch');
setLoading(true);
dummyFetch(complexState).then((resp) => {
console.log('fetch response: ', resp);
setComplexState((s) => ({
...resp,
responseCount: s.responseCount + 1,
}));
setLoading(false);
});
}
if (urlSlug) {
handleSave();
setAutoSave(false)
}
}, [autoSave, complexState]);

Related

Unexpected behaviour of setInterval function (interval keeps on decreasing) [duplicate]

Are there ways to simulate componentDidMount in React functional components via hooks?
For the stable version of hooks (React Version 16.8.0+)
For componentDidMount
useEffect(() => {
// Your code here
}, []);
For componentDidUpdate
useEffect(() => {
// Your code here
}, [yourDependency]);
For componentWillUnmount
useEffect(() => {
// componentWillUnmount
return () => {
// Your code here
}
}, [yourDependency]);
So in this situation, you need to pass your dependency into this array. Let's assume you have a state like this
const [count, setCount] = useState(0);
And whenever count increases you want to re-render your function component. Then your useEffect should look like this
useEffect(() => {
// <div>{count}</div>
}, [count]);
This way whenever your count updates your component will re-render. Hopefully this will help a bit.
There is no exact equivalent for componentDidMount in react hooks.
In my experience, react hooks requires a different mindset when developing it and generally speaking you should not compare it to the class methods like componentDidMount.
With that said, there are ways in which you can use hooks to produce a similar effect to componentDidMount.
Solution 1:
useEffect(() => {
console.log("I have been mounted")
}, [])
Solution 2:
const num = 5
useEffect(() => {
console.log("I will only run if my deps change: ", num)
}, [num])
Solution 3 (With function):
useEffect(() => {
const someFunc = () => {
console.log("Function being run after/on mount")
}
someFunc()
}, [])
Solution 4 (useCallback):
const msg = "some message"
const myFunc = useCallback(() => {
console.log(msg)
}, [msg])
useEffect(() => {
myFunc()
}, [myFunc])
Solution 5 (Getting creative):
export default function useDidMountHook(callback) {
const didMount = useRef(null)
useEffect(() => {
if (callback && !didMount.current) {
didMount.current = true
callback()
}
})
}
It is worth noting that solution 5 should only really be used if none of the other solutions work for your use case. If you do decide you need solution 5 then I recommend using this pre-made hook use-did-mount.
Source (With more detail): Using componentDidMount in react hooks
There's no componentDidMount on functional components, but React Hooks provide a way you can emulate the behavior by using the useEffect hook.
Pass an empty array as the second argument to useEffect() to run only the callback on mount only.
Please read the documentation on useEffect.
function ComponentDidMount() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log('componentDidMount');
}, []);
return (
<div>
<p>componentDidMount: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<div>
<ComponentDidMount />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
useEffect() hook allows us to achieve the functionality of componentDidMount, componentDidUpdate componentWillUnMount functionalities.
Different syntaxes of useEffect() allows to achieve each of the above methods.
i) componentDidMount
useEffect(() => {
//code here
}, []);
ii) componentDidUpdate
useEffect(() => {
//code here
}, [x,y,z]);
//where x,y,z are state variables on whose update, this method should get triggered
iii) componentDidUnmount
useEffect(() => {
//code here
return function() {
//code to be run during unmount phase
}
}, []);
You can check the official react site for more info. Official React Page on Hooks
Although accepted answer works, it is not recommended. When you have more than one state and you use it with useEffect, it will give you warning about adding it to dependency array or not using it at all.
It sometimes causes the problem which might give you unpredictable output. So I suggest that you take a little effort to rewrite your function as class. There are very little changes, and you can have some components as class and some as function. You're not obligated to use only one convention.
Take this for example
function App() {
const [appointments, setAppointments] = useState([]);
const [aptId, setAptId] = useState(1);
useEffect(() => {
fetch('./data.json')
.then(response => response.json())
.then(result => {
const apts = result.map(item => {
item.aptId = aptId;
console.log(aptId);
setAptId(aptId + 1);
return item;
})
setAppointments(apts);
});
}, []);
return(...);
}
and
class App extends Component {
constructor() {
super();
this.state = {
appointments: [],
aptId: 1,
}
}
componentDidMount() {
fetch('./data.json')
.then(response => response.json())
.then(result => {
const apts = result.map(item => {
item.aptId = this.state.aptId;
this.setState({aptId: this.state.aptId + 1});
console.log(this.state.aptId);
return item;
});
this.setState({appointments: apts});
});
}
render(...);
}
This is only for example. so lets not talk about best practices or potential issues with the code. Both of this has same logic but the later only works as expected. You might get componentDidMount functionality with useEffect running for this time, but as your app grows, there are chances that you MAY face some issues. So, rather than rewriting at that phase, it's better to do this at early stage.
Besides, OOP is not that bad, if Procedure-Oriented Programming was enough, we would never have had Object-Oriented Programming. It's painful sometimes, but better (technically. personal issues aside).
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Please visit this official docs. Very easy to understand the latest way.
https://reactjs.org/docs/hooks-effect.html
Info about async functions inside the hook:
Effect callbacks are synchronous to prevent race conditions. Put the async function inside:
useEffect(() => {
async function fetchData() {
// You can await here
const response = await MyAPI.getData(someId);
// ...
}
fetchData();
}, [someId]); // Or [] if effect doesn't need props or state
useLayoutEffect hook is the best alternative to ComponentDidMount in React Hooks.
useLayoutEffect hook executes before Rendering UI and useEffect hook executes after rendering UI. Use it depend on your needs.
Sample Code:
import { useLayoutEffect, useEffect } from "react";
export default function App() {
useEffect(() => {
console.log("useEffect Statements");
}, []);
useLayoutEffect(() => {
console.log("useLayoutEffect Statements");
}, []);
return (
<div>
<h1>Hello Guys</h1>
</div>
);
}
Yes, there is a way to SIMULATE a componentDidMount in a React functional component
DISCLAIMER: The real problem here is that you need to change from "component life cycle mindset" to a "mindset of useEffect"
A React component is still a javascript function, so, if you want something to be executed BEFORE some other thing you must simply need to execute it first from top to bottom, if you think about it a function it's still a funtion like for example:
const myFunction = () => console.log('a')
const mySecondFunction = () => console.log('b)
mySecondFunction()
myFunction()
/* Result:
'b'
'a'
*/
That is really simple isn't it?
const MyComponent = () => {
const someCleverFunction = () => {...}
someCleverFunction() /* there I can execute it BEFORE
the first render (componentWillMount)*/
useEffect(()=> {
someCleverFunction() /* there I can execute it AFTER the first render */
},[]) /*I lie to react saying "hey, there are not external data (dependencies) that needs to be mapped here, trust me, I will leave this in blank.*/
return (
<div>
<h1>Hi!</h1>
</div>
)}
And in this specific case it's true. But what happens if I do something like that:
const MyComponent = () => {
const someCleverFunction = () => {...}
someCleverFunction() /* there I can execute it BEFORE
the first render (componentWillMount)*/
useEffect(()=> {
someCleverFunction() /* there I can execute it AFTER the first render */
},[]) /*I lie to react saying "hey, there are not external data (dependencies) that needs to be maped here, trust me, I will leave this in blank.*/
return (
<div>
<h1>Hi!</h1>
</div>
)}
This "cleverFunction" we are defining it's not the same in every re-render of the component.
This lead to some nasty bugs and, in some cases to unnecessary re-renders of components or infinite re-render loops.
The real problem with that is that a React functional component is a function that "executes itself" several times depending on your state thanks to the useEffect hook (among others).
In short useEffect it's a hook designed specifically to synchronize your data with whatever you are seeing on the screen. If your data changes, your useEffect hook needs to be aware of that, always. That includes your methods, for that it's the array dependencies.
Leaving that undefined leaves you open to hard-to-find bugs.
Because of that it's important to know how this work, and what you can do to get what you want in the "react" way.
const initialState = {
count: 0,
step: 1,
done: false
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'doSomething') {
if(state.done === true) return state;
return { ...state, count: state.count + state.step, state.done:true };
} else if (action.type === 'step') {
return { ...state, step: action.step };
} else {
throw new Error();
}
}
const MyComponent = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
dispatch({ type: 'doSomething' });
}, [dispatch]);
return (
<div>
<h1>Hi!</h1>
</div>
)}
useReducer's dispatch method it's static so it means it will be the same method no matter the amount of times your component is re-rendered. So if you want to execute something just once and you want it rigth after the component is mounted, you can do something like the above example. This is a declarative way of do it right.
Source: The Complete Guide to useEffect - By Dan Abramov
That being said if you like to experiment with things and want to know how to do it "the imperative wat" you can use a useRef() with a counter or a boolean to check if that ref stores a defined reference or not, this is an imperative approach and it's recommended to avoid it if you're not familiar with what happen with react behind curtains.
That is because useRef() is a hook that saves the argument passed to it regardless of the amount of renders (I am keeping it simple because it's not the focus of the problem here, you can read this amazing article about useRef ). So it's the best approach to known when the first render of the component happened.
I leave an example showing 3 different ways of synchronise an "outside" effect (like an external function) with the "inner" component state.
You can run this snippet right here to see the logs and understand when these 3 functions are executed.
const { useRef, useState, useEffect, useCallback } = React
// External functions outside react component (like a data fetch)
function renderOnce(count) {
console.log(`renderOnce: I executed ${count} times because my default state is: undefined by default!`);
}
function renderOnFirstReRender(count) {
console.log(`renderOnUpdate: I executed just ${count} times!`);
}
function renderOnEveryUpdate(count) {
console.log(`renderOnEveryUpdate: I executed ${count ? count + 1 : 1} times!`);
}
const MyComponent = () => {
const [count, setCount] = useState(undefined);
const mounted = useRef(0);
// useCallback is used just to avoid warnings in console.log
const renderOnEveryUpdateCallBack = useCallback(count => {
renderOnEveryUpdate(count);
}, []);
if (mounted.current === 0) {
renderOnce(count);
}
if (mounted.current === 1) renderOnFirstReRender(count);
useEffect(() => {
mounted.current = mounted.current + 1;
renderOnEveryUpdateCallBack(count);
}, [count, renderOnEveryUpdateCallBack]);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(prevState => (prevState ? prevState + 1 : 1))}>TouchMe</button>
</div>
);
};
class App extends React.Component {
render() {
return (
<div>
<h1>hI!</h1>
</div>
);
}
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<MyComponent/>
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
If you execute it you will see something like this:
You want to use useEffect(), which, depending on how you use the function, can act just like componentDidMount().
Eg. you could use a custom loaded state property which is initially set to false, and switch it to true on render, and only fire the effect when this value changes.
Documentation
the exact equivalent hook for componentDidMount() is
useEffect(()=>{},[]);
hope this helpful :)

react-hooks/exhaustive-deps and empty dependency lists for "on mount" [duplicate]

This is a React style question.
TL;DR Take the set function from React's useState. If that function "changed" every render, what's the best way to use it in a useEffect, with the Effect running only one time?
Explanation We have a useEffect that needs to run once (it fetches Firebase data) and then set that data in application state.
Here is a simplified example. We're using little-state-machine, and updateProfileData is an action to update the "profile" section of our JSON state.
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
get_data_async();
}, []);
return (
<p>Hello, world!</p>
);
}
However, ESLint doesn't like this:
React Hook useEffect has a missing dependency: 'actions'. Either include it or remove the dependency array
Which makes sense. The issue is this: actions changes every render -- and updating state causes a rerender. An infinite loop.
Dereferencing updateProfileData doesn't work either.
Is it good practice to use something like this: a single-run useEffect?
Concept code that may / may not work:
const useSingleEffect = (fxn, dependencies) => {
const [ hasRun, setHasRun ] = useState(false);
useEffect(() => {
if(!hasRun) {
fxn();
setHasRun(true);
}
}, [...dependencies, hasRun]);
};
// then, in a component:
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
useSingleEffect(async () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
}, [actions]);
return (
<p>Hello, world!</p>
);
}
But at that point, why even care about the dependency array? The initial code shown works and makes sense (closures guarantee the correct variables / functions), ESLint just recommends not to do it.
It's like if the second return value of React useState changed every render:
const [ foo, setFoo ] = useState(null);
// ^ this one
If that changed every render, how do we run an Effect with it once?
Ignore the eslint rule for line
If you truly want the effect to run only once exactly when the component mounts then you are correct to use an empty dependency array. You can disable the eslint rule for that line to ignore it.
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
get_data_async();
// NOTE: Run effect once on component mount, please
// recheck dependencies if effect is updated.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Note: If you later update the effect and it needs to run after other dependencies then this disabled comment can potentially mask future bugs, so I suggest leaving a rather overt comment as for the reason to override the established linting rule.
Custom hook logic
Alternatively you can use a react ref to signify the initial render. This is preferable to using some state to hold the value as updating it would trigger unnecessary render.
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
const initialRenderRef = useRef(true);
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
if (initialRenderRef.current) {
initialRenderRef.current = false;
get_data_async();
}
}, [actions]); // <-- and any other dependencies the linter complains about
return (
<p>Hello, world!</p>
);
}
And yes, absolutely you can factor this "single-run logic" into a custom hook if it is a pattern you find used over and over in your codebase.

refetch in reactQuery is not return the data

I am using reactQuery in my react application. I need to call one get API in button click. for that i am using refetch option in reactQuery. API call is working fine but my response data is coming undefined. I checked in browser network there i can see the response.
My API call using reactQuery
const { data: roles, refetch: roleRefetch } = useQuery('getRoles', () => api.getRoles('ID_234'), { enabled: false });
My click event
const handleAdd = (e) => { roleRefetch(); console.log(roles) }
My action call using axios
export const getRoles = (name) => axios.get(roles/list?sa_id=${name}, { headers: setHeader }).then(res => res);
const handleAdd = (e) => { roleRefetch(); console.log(roles) }
this not how react works, and it's not react-query specific. calling a function that updates some state will not have your state be available in the next line. It will make it available in the next render cycle. Conceptually, you want this to work, which cannot with how react is designed:
const [state, setState] = React.useState(0)
<button onClick={() => {
setState(1)
console.log(state)
}}
here, the log statement will log 0, not 1, because the update doesn't happen immediately, and this is totally expected.
With react-query, what you can do is await the refetch, because its async, and it will give you the result back:
const handleAdd = async (e) => {
const { data } = await roleRefetch();
console.log(data)
}
or, depending on what you actually want to do, you can:
use data in the render function to render something - it will always be up-to-date.
use theonSuccess callback of useQuery to trigger side-effects whenever data is fetched
spawn a useEffect in the render function that does the logging:
const { data: roles, refetch: roleRefetch } = useQuery('getRoles', () => api.getRoles('ID_234'), { enabled: false });
React.useEffect(() => {
console.log(roles)
}, [roles])
on a more general note, I think disabling a query and then calling refetch on a button click is very likely not idiomatic react-query. Usually, you have some local state that drives the query. in your case, that's likely the id. Dependencies of the query should go to the queryKey, and react-query will trigger a refetch automatically when the key changes. This will also give you caching by id. You can use enabled to defer querying when your dependencies are not yet ready. Here's what I would likely do:
const [id, setId] = React.useState(undefined)
const { data: roles } = useQuery(['getRoles', id], () => api.getRoles(id), { enabled: !!id });
const handleAdd = (e) => { setId('ID_234') }
of course, id doesn't have to come from local state - it could be some other form of client state as well, e.g. a more global one.

How to prevent set sate in unmounted component in React?

I received a warning: "Can't perform a React state update on an unmounted component", so I try to determine when my component is unmounted, like below:
function ListStock() {
let mounted = true;
const [data, setData] = useState([]);
const [search, setSearch] = useState();
useEffect(() => {
async function fetchData() {
const {start_date, end_date} = search;
const result = await getDataStock(start_date, end_date);
if (result && mounted) {
setData(result.data); // only set a state when mounted = true
}
}
fetchData();
return () => {
mounted = false; // set false on clean up
}
}, [search])
const handleSearch = () => {
...
setSearch({
start_date: moment().subtract(1, 'month').format('YYYY-MM-DD'),
end_date: moment().format('YYYY-MM-DD')
});
}
return (
<div>
<input type="text" id="keyword">
<input type="button" onlick={handleSearch} value="Search">
{data}
</div>
)
}
By this way, it can resolve that warning message, however it shows another one:
"Assignments to the 'mounted' variable from inside React Hook
useEffect will be lost after each render. To preserve the value over
time, store it in a useRef Hook and keep the mutable value in the
'.current' property. Otherwise, you can move this variable directly
inside useEffect"
When I store the 'mounted' variable in a useRef hook, I cannot search anymore, since the 'mounted' is always set to "false".
My questions are:
Why a clean up code runs when User click a search button? I though it runs only when a component is unmounted?
What is the right way to implement a searching job with a remote api?
Is it fine if I config ESLint to ignore all this kind of warning messages?
Thanks all.
The problem is that you are doing the whole mounted/unmounted thing wrong. Here is a proper implementation:
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []); // Notice lack of dependencies
Before I go on, I should probably refer you to the awesome react-use library, which already comes with a useMountedState hook
Now back to your questions
Why a clean up code runs when User click a search button? I though it
runs only when a component is unmounted?
I didn't realize this was a thing until I read the docs:
When exactly does React clean up an effect? React performs the cleanup
when the component unmounts. However, as we learned earlier, effects
run for every render and not just once. This is why React also cleans
up effects from the previous render before running the effects next
time...
So there you have it: The cleanup function is run after every render which happens after state changes, thus when search changes, a re-render is required.
What is the right way to implement a searching job with a remote api?
The way you are doing it is fine, but if you are going to be checking for unmounted state every time, you might as well use the library I mentioned.
Is it fine if I config ESLint to ignore all this kind of warning
messages?
Nah. Just fix it. It is very easy
Instead of using a variable, you need to store mounted = true; in a useRef hook. UseRef can hold values and it won't re-render the page when the value changes.
function ListStock() {
const mounted = useRef(true);
const [data, setData] = useState([]);
const [search, setSearch] = useState();
useEffect(() => {
async function fetchData() {
const {start_date, end_date} = search;
const result = await getDataStock(start_date, end_date);
if (result && mounted,current) {
setData(result.data); // only set a state when mounted = true
}
}
fetchData();
return () => {
mounted.current = false; // set false on clean up
}
}, [search])
const handleSearch = () => {
...
setSearch({
start_date: moment().subtract(1, 'month').format('YYYY-MM-DD'),
end_date: moment().format('YYYY-MM-DD')
});
}
return (
<div>
<input type="text" id="keyword">
<input type="button" onlick={handleSearch} value="Search">
{data}
</div>
)
}
Hopefully, questions 1 and 2 will be solved by the above code. 3rd question, I would say it's better to keep it as it shows what's going wrong.
in this case, put mounted inside useEffect is better,
once search changed, previous request should be cancel,
const [data, setData] = useState([]);
const [search, setSearch] = useState();
useEffect(() => {
let cancel = true;
async function fetchData() {
const {start_date, end_date} = search;
const result = await getDataStock(start_date, end_date);
if (result && !cancel) {
setData(result.data); // only set a state when not canceled
}
}
fetchData();
return () => {
cancel = true; // to cancel setState
}
}, [search])

Is it wrong to fetch an API without using the useEffect Hook?

I've been doing it this way but some colleges told me that I should use the useEffect Hook instead. The problem is that I don't see the benefit of that approach and I think that my approach is cleaner.
import React, { useState, useEffect } from "react";
const fetchTheApi = () =>
new Promise(res => setTimeout(() => res({ title: "Title fetched" }), 3000));
const UseEffectlessComponent = () => {
const [data, setData] = useState();
!data && fetchTheApi().then(newData => setData(newData));
return <h1>{data ? data.title : "No title"}</h1>;
};
const UseEffectComponent = () => {
const [data, setData] = useState();
useEffect(() => {
fetchTheApi().then(newData => setData(newData));
}, []);
return <h1>{data ? data.title : "No title"}</h1>;
};
const MyComponent = () => (
<div>
<UseEffectlessComponent />
<UseEffectComponent />
</div>
);
Edit based on responses:
I changed the code to re render, like this:
import React, { useState, useEffect } from 'react';
const fetchTheApi = (origin) => {
console.log('called from ' + origin);
return new Promise((res) =>
setTimeout(() => res({ title: 'Title fetched' }), 3000)
);
};
const UseEffectlessComponent = () => {
const [data, setData] = useState();
!data &&
fetchTheApi('UseEffectlessComponent').then((newData) => setData(newData));
return <h1>{data ? data.title : 'No title'}</h1>;
};
const UseEffectComponent = () => {
const [data, setData] = useState();
useEffect(() => {
fetchTheApi('UseEffectComponent').then((newData) => setData(newData));
}, []);
return <h1>{data ? data.title : 'No title'}</h1>;
};
const MyComponent = () => {
const [counter, setCounter] = useState(0);
counter < 3 && setTimeout(() => setCounter(counter + 1), 1000);
return (
<div>
<p>counter is: {counter}</p>
<UseEffectlessComponent />
<UseEffectComponent />
</div>
);
};
In the console I got:
called from UseEffectlessComponent
called from UseEffectComponent
called from UseEffectlessComponent
called from UseEffectlessComponent
called from UseEffectlessComponent
So, I finally found the benefit to that approach. I've got some code to change... Thanks a lot for the answers!
How you've written it does work, kind of. You're saying "If the fetch fails and the component re-renders, then try again, else don't". Personally I think that is an unreliable system - depending on a re-render to try again, and can easily have unintended side-effects:
What if your data is falsy? What if it fails (which you didn't handle). In this case it will keep trying to re-fetch.
What if the parent renders 3 times in a row (a very common situation). In that case your fetch will happen 3 times before the first fetch is complete.
So with that in mind you actually need more careful checks to ensure you code doesn't have unexpected consequences by not using useEffect. Also if your fetch wanted to re-fetch on prop changes your solution also doesn't work.
Right now, if your component re-renders before it has set the data it will attempt to fetch the data again leading to multiple fetches. Considering you only want to fetch data once and not accidentally multiple times it would be better to put it in the useEffect.
You should use useEffect, because what you do is anti-pattern. From react's website you can clearly see why useEffect is there:
Data fetching, setting up a subscription, and manually changing the
DOM in React components are all examples of side effects. Whether or
not you’re used to calling these operations “side effects” (or just
“effects”), you’ve likely performed them in your components before.
https://reactjs.org/docs/hooks-effect.html
React components are just functions, takes some props and returns some jsx. If you want to have a side effect, you shouldn't have it directly in your component. It should be in a lifecycle method.
Image your condition check (!data), was a complex one looping over arrays etc. It'd have a bigger performance impact. But useEffect will be more performant and you can even use the second argument for kind of 'caching' results.
There is technically no difference between your two components, except the condition check will run on every render in your version. Whereas useEffect will be called only in 'mounted', 'updated' states of the component.

Resources