How to prevent race conditions with react useState hook - javascript

I have a component that uses hooks state (useState) api to track the data.
The object looks like this
const [data,setData] = React.useState({})
Now I have multiple buttons which make API requests and set the data with the new key
setAPIData = (key,APIdata) => {
const dup = {
...data,
[key]:APIdata
}
setData(dup)
}
Now if I make multiple requests at the same time , it results in race conditions since setting state in react is asynchronous and I get the previous value.
In class-based components, we can pass an updater function to get the updated value, how to do this hooks based component.

You must use setData with a function as its argument. Then it will always get the previous state, no matter what order it will be called.
const [data,setData] = React.useState({})
setData(prevData => ({
...prevData,
[key]: APIdata
}));
Documentation: somewhat hidden in hook api reference.
reactjs.org/docs/hooks-reference.html#functional-updates

Related

Correct way to change router state

I have a router state "refetch" that triggers the component to refetch its data (instead of using the cached one), but when it's done, I'd like to toggle that state back to avoid an infinite loop. What is the best way to change state parameters? Or this approach is wrong?
const { state } = useLocation<{ refetch: boolean }>()
const query = useQuery<....>()
useEffect(() => {
if (state.refetch) {
query.refetch()
state.refetch = false // feels wrong to me
}
}, [query, state])
I think instead of mutating the route state object you could issue a redirect with the refetch state set false.
Example:
const history = useHistory();
const { pathname, state } = useLocation();
const query = useQuery<....>();
useEffect(() => {
if (state.refetch) {
query.refetch();
history.replace(pathname, { refetch: false });
}
}, [query, state]);
If you're using React Query, you don't need to add an useEffect for that, you can add dependencies to a query, passing an array as the query key, eg:
useQuery(['key', ...dependencies], queryFn);
Every time the dependencies change it will refetch the query.
In your case you could try useQuery(['key', state], queryFn);
If you need to stick to the refetch of the history, you could add the toggle to the queryFn
Query Keys - React Query
If your function depends on a variable include it in your query key - Stack Overflow
PD: You shouldn't use an object or array as a dependency for useEffect or useMemo, etc. That will always cause an infinite loop.
You could stringify them first using JSON.stringify(foo), use a property of the object, maybe the length of the array. Personally, I stringify them, that way the comparison is more accurate.

Can't find the way to useSelector from redux-toolkit within event handler and pass params to it

There is an event handler for click and when it triggered i want to pull specific data from redux using selector where all logic many-to-many is implemented. I need to pass id to it in order to receive its individual data. Based on rules of the react the hooks can be called in function that is neither a React function component nor a custom React Hook function.
So what is the way to solve my problem ?
const handleMediaItemClick = (media: any): void => {
// For example i check media type and use this selector to pull redux data by id
const data = useSelector(playlistWithMediaSelector(imedia.id));
};
As stated in the error message, you cannot call hooks inside functions. You call a hook inside a functional component and use that value inside the function. The useSelector hook updates the variable each time the state changes and renders that component.
Also, when you get data with useSelector, you should write the reducer name you need from the redux state.
const CustomComponent = () => {
// The data will be updated on each state change and the component will be rendered
const data = useSelector((state) => state.REDUCER_NAME);
const handleMediaItemClick = () => {
console.log(data);
};
}
You can check this page for more information.https://react-redux.js.org/api/hooks#useselector
You should probably use local state value to track that.
const Component = () => {
const [imediaId, setImediaId] = useState(null);
const data = useSelector(playlistWithMediaSelector(imediaId));
function handleMediaClick(id) {
setImediaId(id)
}
useEffect(() => {
// do something on data
}, [imediaId, data])
return <div>...</div>
}
Does that help?
EDIT: I gather that what you want to do is to be able to call the selector where you need. Something like (considering the code above) data(id) in handleMediaClick. I'd bet you gotta return a curried function from useSelector, rather than value. Then you would call it. Alas, I haven't figured out how to that, if it's at all possible and whether it's an acceptable pattern or not.

Updating state causing tests to not stop

I have a React application. I am using Jest and React Testing library for unit testing.
I have to test a component. In the useEffect of the component, there is an API call made and once the response is received, we update the component's local state.
const [data, setData] = useState({})
useEffect(()=>{
// Make API call to a custom fetch hook
},[])
useEffect(()=>{
setData(response.data) //response data is a JSON object
},[response])
The test files code snippet is as below -
const {getByTestId} = render(<MyComponent></MyComponent>)
I have not put any assertions yet because of the inifinite running test cases
What have I done? I have been able to mock the fetch call and execute setData.
The problem - The tests keep running forever. But if I change the response.data to some boolean or string or number, the tests do not run infinitly.
Also, if I put a dummy object in the initialization of the state, the tests run fine.
const [data, setData] = useState({
name: 'Test',
Age: '99'
})
Providing an object as dependency in useEffect is not a good idea, since even if the data in object remains same, on every render -- object reference changes - the effect will run again (even if the data within stays same).
A workaround for this would be stringifying the dependency with JSON.stringify. (although doing on data containing some objects like dates, symbols, null or undefined etc. isn't recommended)
useEffect(() => {
setData(response.data)
}, [JSON.stringify(response)]);
Doing above shouldn't affect your UI.
Other solution would be to store the previous value of response and compare before you do setData. You can use usePrevious hook:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}

Fetch data and setState every time just before component renders in React Native

There is the case , i have a modal which shows some data from its state ( an array ) , and it's state is getting set in componentDidMount() function like docs suggests. I need to show updated data every time when modal opened up.So i was able to do that with componentWillReceiveProps(nextProps) function , like i showed below .
But what if i want to migrate to getDerivedStateFromProps function ? How am i going to achieve same behaviour with it ?
Here is the component code simplified for sake :
export class PastOrdersModal extends Component {
constructor(props) {
super(props);
this.state = {
past: {},
isLoading: false,
modalVisible: false
};
}
async componentWillReceiveProps(nextProps) {
const response = await this.fetchPast();
this.setState({ past: response });
}
async componentDidMount() {
const response = await this.fetchPast();
this.setState({ past: response });
}
...
... some callback functions for handling modalvisible value ....
...
render(){
// here rendering state array with this.state.past.map(......) etc.
}
The fetchPast function makes a GET request to server of mine , all you need to know that it returns an array to me. This is working perfectly. ComponentWillReceiveProps gets called every time because parent component sends modalVisible props everytime.
But componentWillRecieveProps is deprecating and i could not make the same behavior with getDerivedStateFromProps.How should i implement same thing with it.
Note: ı am not going to use redux ,i am not going to use mobx , i know how to use them and it's not what i want. The thing is the behavior i want is soo simple i don't want to pass values another component , i don't want to pass values another screen , i just want to update a simple component that is all but either the framework is pushing it's limits to make simplest tasks hardests thing ever or i am missing really huge point.(probably latter one :) )
Note: I know i am not doing anything with nextProps , that was the only solution i found.
You can write your Component as a function so you can use React Hooks
const PastOrdersModal = (props) => {
const [past, setPast] = useState({});
const [isLoading, setLoading] = useState(false);
const [modalVisible, setModalVisibility] = useState(false);
useEffect(() => {
const fetchPast = async () => {
const response = await fetchPast();
setPast(response);
};
if(modalVisible) fetchPast();
}, [modalVisible])
useEffect(() => {
const fetchPast = async () => {
const response = await fetchPast();
setPast(response);
};
fetchPast();
}, [])
...
... some callback functions for handling modalvisible value ....
...
return <YourComponent />
The variables we created inside [] at the top are the ones we are using for the state. The first one will be the state as itself and the second one is a function responsible for updating that state. The useEffect hook will simulate the life cycle methods for Class Components, it receives a callback that will be executed and a second argument. The second argument is the one that will indicate when it is going to be triggered. For instance, you can see 2 useEffects, the one with the empty array will tell the hook to execute just once, similar as componentDidMount. The other one will be triggered when modalVisible changes, so everytime you change its value it will be executed, that's why we only validate if the modal is visible (is true) we do the fetch, otherwise the fetch won't be executed

Fetch graphql query result on render and re-renders BUT LAZILY

I have a React Apollo app and what I am trying to do is that I have a component that renders some data using charts. For this data, I have some filters that I save in the local state of the component (Using hooks)
const [filters, setFilters] = useState(defaultFilters);
Now what I want is that whenever the component mounts, fetch the data using the default filters. But I also want to re-fetch data when the user updates the filters AND CLICKS ON SUBMIT and I'd fetch the results using new filters.
Since I also want to fetch the results on filter update, I am using useLazyQuery hook provided by apollo
const [getData, {data}] = useLazyQuery(GET_DATA_QUERY, { variables: {filters} });
useEffect(getData, []); // this useEffect runs only when the component mounts and never again
But, what happens is whenever my state, filters, updates the getData function is automatically run! ALWAYS! (BEHIND THE SCENE)
How do I handle such cases, where I want to fetch results on mounting and re-rendering.
I have tried using useQuery and refetch provided by it but I get the same problem there, whenever I update the state, the component rerenders and the useQuery hooks is run and makes the call. (That's how I believe it runs)
How do I fix my current code. Calling the getData function inside the useEffect function makes it run on every re-render.
I think I the problem defined in this stackoverflow-question is somewhat similar to mine.
Part of the problem is that you really have two different states that you're trying to utilize a single hook for. You have state that represents your inputs' values in the UI, and then you have state that represents the filters you want to actually apply to your charts. These are two separate bits of state.
The simplest solution is to just do something like this:
const [inputFilters, setInputFilters] = useState(defaultFilters)
const [appliedFilters, setAppliedFilters] = useState(inputFilters)
const { data } = useQuery(GET_DATA_QUERY, { variables: { filters: appliedFilters } })
const handleSubmit = () => setAppliedFilters(inputFilters)
const handleSomeInputChange = event => setInputFilters(...)
This way, you use inputFilters/setInputFilters only to manage your inputs' state. When the user clicks your submit button, the appliedFilters are set to whatever the inputFilters are at the time, and your query will update to reflect the new variables.

Categories