Stop `useMutation` from re-rendering component and ignore result - reactjs

I have a mutation called like this (this isn't the actual mutation call, but a minimal example):
const App = () => {
const [myMutation] = useMutation(gql`
mutation Test($updateUserId: ID!, $updateUserInput: UpdateUserInput!) {
updateUser(id: $updateUserId, input: $updateUserInput) {
id
firstname
age
}
}
`);
// Runs every time this component is rendered
console.log("rendered");
// Called when the button is clicked
const update = () => {
myMutation({
variables: {
updateUserId: 1,
updateUserInput: {
age: Math.round(Math.random() * 10 + 5) // Set a random age from 5 - 15
}
}
});
};
return (
<>
<h1>Update Test</h1>
<button onClick={update}>Update!</button>
</>
);
};
Whenever onClick is called, the whole component is re-rendered. This is not what I want, as I don't care about the result of the mutation. Is there any way to stop myMutation from causing a re-render, and ignore the result completely?

useMutation is a hook and it has state in it which cause a re-render
See the useMultation type bellow:
export interface MutationResult<TData = any> {
data?: TData | null;
error?: ApolloError;
loading: boolean;
called: boolean;
client: ApolloClient<object>;
}
The loading state is the one causing re-render, so in which component you delcare useMutation it will re-render due to that state
One possible fix is to separate the component with useMutation to another one
Something like this:
function App() {
return (
<>
<Header /> // <-- this coponent won't re-render
<UpdateUserButton /> // <-- useMutation is in here!
</>
);
}
Checkout the live example:

Well, if you don't need the data returned by the mutation, you can also pass the ignoreResults option. Data will not be returned from the mutation hook but loading would called neither which prevents a re-render.
https://www.apollographql.com/docs/react/data/mutations/#ignoreresults
const [someMutation] = useSomeMutation({
ignoreResults: true,
});

This is likely because the mutation is returning a normalised object which this component or a higher component depends on via a hook like useQuery.
If you don't care about the result of the mutation make sure it doesn't return the normalised object.
Something like
mutation SomeMutation ($input: SomeMutationInput!){
someMutation (input: $input){
ok
}
}

Following should work (not tested)
Since you don't want re-render, you don't need to use useMutation hook which has state.
Instead you can directly import you apollo-client and use mutate on it without using useMutation hook.
You can choose to use the result in .then function or get rid of it.
import { apolloClient } from '../yourApolloClientFile';
const App = () => {
const myMutation = (vars) => apolloClient.mutate({
mutation: gql`
mutation Test($updateUserId: ID!, $updateUserInput: UpdateUserInput!) {
updateUser(id: $updateUserId, input: $updateUserInput) {
id
firstname
age
}
}
`,
variables: vars
})
.then(result => console.log(result))
.catch(error => console.log(error))
// Runs every time this component is rendered
console.log("rendered");
// Called when the button is clicked
const update = () => {
myMutation({
updateUserId: 1,
updateUserInput: {
age: Math.round(Math.random() * 10 + 5) // Set a random age from 5 - 15
}
});
};
return (
<>
<h1>Update Test</h1>
<button onClick={update}>Update!</button>
</>
);
};

Related

React context state not being updated

I have a basic react context, similar to below:
function IdProvider({ children }: any) {
const [id, setId] = useState("DEFAULT_ID")
return (
<IdContext.Provider value={{ id, setId }}>
{children}
</IdContext.Provider>
)
}
I'm wrapping all of my routes with this provider, and have one component as below which I want to use to update the Id:
function UpdateForm() {
const { id, setId } = useId() // wrapper for the IdContext
const moveToDisplay = (newId: string) => {
setId(newId)
window.location.href = "/display/" + id
}
return (
<>
<span onClick={() => moveToDisplay("NEW_ID")}>
Update and redirect
</span>
</>
)
}
Upon redirecting, this component is used:
function DisplayId(): JSX.Element {
const { id } = useId()
useEffect(() => {
document.title = id
}, [id])
return (
<>
{id}
</>
)
}
The issue is, the initial setId(newId) doesn't update the state, so when window.location.href = "/display/" + id is called it redirects to /display/DEFAULT_ID, and the DisplayId component still uses the DEFAULT_ID value when rendering. From my understanding the useState is asynchronous, so I'm not entirely sure how I should approach this problem. I've tried adding a 5 second timeout after setId is called, but the value of id is still the default value.
EDIT - SOLVED
Figured out the issue and it was fairly unrelated. I was using a single constant (e.g. DEFAULT_ID) to initialise state in various places, without realising that React checks for referential equality when updating / re-rendering.
useContext provides the ability to pass properties through the react chain of components. So change your provider to this & make sure it's exporting
<IdContext.Provider value={id}>
then in your child you can update that by importing the Context:
import IdContext from './IdProvider'
and useContext:
const [context, setContext] = useContext(IdContext)
then set it with the new value:
const moveToDisplay = (newId: string) => {
setContext(newId)
window.location.href = "/display/" + newId
}
After that you import it in the same fashion through your DisplayId and just use the value

How Exactly useselector works?

As per Doc
useSelector(selector: Function, equalityFn?: Function)
useSelector accepts two params and second one is optional since by default it compare strict === reference equality check but not shallow equality.
const state = useSelector(state => {
console.log("useSelector rerendering");
return state.counter
}, shallowEqual)
Another is
const state = useSelector(state => {
console.log("useSelector rerendering");
return state.counter
})
In both Cases component rerendering when redux store state changes and also when local state changes then it will render (print console.log inside useSelector)
I really didn't understand exactly how it works
Full source code
import React, { useState } from 'react'
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
import { decrement, increment } from './store/actions'
export default function CounterHooks(props) {
const [submit, setSubmit] = useState(false)
const state = useSelector(state => {
console.log("useSelector rerendering");
return state.counter
}, shallowEqual)
const dispatch = useDispatch()
console.log("component rerendering");
const increments = () => {
dispatch(increment());
}
const decrements = () => {
dispatch(decrement());
}
const submitButton = () => {
console.log("component submit", submit);
setSubmit((previousState) => !previousState)
}
return (
<div>
<button onClick={increments} >Incrmeent Counter</button>
<br />
<button onClick={decrements} >Decrement Counter</button>
<br />
<button onClick={submitButton} >Submit</button>
<br />
<h2>total : {state.count}</h2> <br />
<h2>Submit:{String(submit)}</h2> <br />
</div>
)
}
MY question is how exactly second param works ?
In your example, it does not make a difference.
shallowEquals makes sense when you select an object that might be similar in contents, but different by reference.
See these two objects:
const a = { foo: "bar" }
const b = { foo: "bar" }
console.log( a === b ) // will log false
console.log( shallowEquals(a, b)) // will log true
While a and b are two objects with similar shape and contents, they are not the same object.
Now shallowEquals does a === comparison between a.foo and b.foo and since both are strings with the same content, a.foo === b.foo will be true.
This does play a role if you create a new object in your selector, say
const result = useSelector((state) => {
return { a: state.foo.bar, b: state.baz.boo }
})
The result of this will always be a new object, so per default useSelector will always assume they are different, even when state.foo.bar and state.baz.boo actually did not change.
If you use a shallowEqual, useSelector will look at the direct (only 1 level deep) child properties of the objects and compare those. Then it will notice that they are in fact equal and not rerender.
Consider the below as reducer,
const userSlice = (
{name: "user", initialState: {name: "test", isLoggedIn: true},
reducers: {updateUser(state, action) => (state.isLoggedIn=action.payload["isLoggedIn"])}
}
If we dispatch an action to reducer with same data like below,
const result = useSelector(state => state.users)
dispatch({isLoggedIn: true})
The above code will not cause the component to re-render, since we the state.users we are referring is same as previous one.
We can make it to re-render with the below changes,
const result = useSelector(state => ({name: state.users.name, isLoggedIn: state.users.isLoggedIn)
dispatch({isLoggedIn: true})
In the above code, we are returning an object directly so whenever the useSelector runs the returned objects will not be same due to referential equality. This make component to re-render
With useSelector you can also override the default strategy for rendering.
This hook takes an optional equality comparison function as the second parameter that allows you to customize the way the selected state is compared to determine whether the component needs to be re-rendered.*
https://github.com/reduxjs/react-redux/blob/master/src/hooks/useSelector.ts

Update useState immediately

useState does not update the state immediately.
I'm using react-select and I need to load the component with the (multi) options selected according to the result of the request.
For this reason, I created the state defaultOptions, to store the value of the queues constant.
It turns out that when loading the component, the values ​​are displayed only the second time.
I made a console.log in the queues and the return is different from empty.
I did the same with the defaultOptions state and the return is empty.
I created a codesandbox for better viewing.
const options = [
{
label: "Queue 1",
value: 1
},
{
label: "Queue 2",
value: 2
},
{
label: "Queue 3",
value: 3
},
{
label: "Queue 4",
value: 4
},
{
label: "Queue 5",
value: 5
}
];
const CustomSelect = (props) => <Select className="custom-select" {...props} />;
const baseUrl =
"https://my-json-server.typicode.com/wagnerfillio/api-json/posts";
const App = () => {
const userId = 1;
const initialValues = {
name: ""
};
const [user, setUser] = useState(initialValues);
const [defaultOptions, setDefaultOptions] = useState([]);
const [selectedQueue, setSelectedQueue] = useState([]);
useEffect(() => {
(async () => {
if (!userId) return;
try {
const { data } = await axios.get(`${baseUrl}/${userId}`);
setUser((prevState) => {
return { ...prevState, ...data };
});
const queues = data.queues.map((q) => ({
value: q.id,
label: q.name
}));
// Here there is a different result than emptiness
console.log(queues);
setDefaultOptions(queues);
} catch (err) {
console.log(err);
}
})();
return () => {
setUser(initialValues);
};
}, []);
// Here is an empty result
console.log(defaultOptions);
const handleChange = async (e) => {
const value = e.map((x) => x.value);
console.log(value);
setSelectedQueue(value);
};
return (
<div className="App">
Multiselect:
<CustomSelect
options={options}
defaultValue={defaultOptions}
onChange={handleChange}
isMulti
/>
</div>
);
};
export default App;
React don't update states immediately when you call setState, sometimes it can take a while. If you want to do something after setting new state you can use useEffect to determinate if state changed like this:
const [ queues, setQueues ] = useState([])
useEffect(()=>{
/* it will be called when queues did update */
},[queues] )
const someHandler = ( newValue ) => setState(newValue)
Adding to other answers:
in Class components you can add callback after you add new state such as:
this.setState(newStateObject, yourcallback)
but in function components, you can call 'callback' (not really callback, but sort of) after some value change such as
// it means this callback will be called when there is change on queue.
React.useEffect(yourCallback,[queue])
.
.
.
// you set it somewhere
setUserQueues(newQueues);
and youre good to go.
no other choice (unless you want to Promise) but React.useEffect
Closures And Async Nature of setState
What you are experiencing is a combination of closures (how values are captured within a function during a render), and the async nature of setState.
Please see this Codesandbox for working example
Consider this TestComponent
const TestComponent = (props) => {
const [count, setCount] = useState(0);
const countUp = () => {
console.log(`count before: ${count}`);
setCount((prevState) => prevState + 1);
console.log(`count after: ${count}`);
};
return (
<>
<button onClick={countUp}>Click Me</button>
<div>{count}</div>
</>
);
};
The test component is a simplified version of what you are using to illustrate closures and the async nature of setState, but the ideas can be extrapolated to your use case.
When a component is rendered, each function is created as a closure. Consider the function countUp on the first render. Since count is initialized to 0 in useState(0), replace all count instances with 0 to see what it would look like in the closure for the initial render.
const countUp = () => {
console.log(`count before: ${0}`);
setCount((0) => 0 + 1);
console.log(`count after: ${0}`);
};
Logging count before and after setting count, you can see that both logs will indicate 0 before setting count, and after "setting" count.
setCount is asynchronous which basically means: Calling setCount will let React know it needs to schedule a render, which it will then modify the state of count and update closures with the values of count on the next render.
Therefore, initial render will look as follows
const countUp = () => {
console.log(`count before: 0`);
setCount((0) => 0 + 1);
console.log(`count after: 0`);
};
when countUp is called, the function will log the value of count when that functions closure was created, and will let react know it needs to rerender, so the console will look like this
count before: 0
count after: 0
React will rerender and therefore update the value of count and recreate the closure for countUp to look as follows (substituted the value for count).This will then update any visual components with the latest value of count too to be displayed as 1
const countUp = () => {
console.log(`count before: 1`);
setCount((1) => 1 + 1);
console.log(`count after: 1`);
};
and will continue doing so on each click of the button to countUp.
Here is a snip from codeSandbox. Notice how the console has logged 0 from the intial render closure console log, yet the displayed value of count is shown as 1 after clicking once due to the asynchronous rendering of the UI.
If you wish to see the latest rendered version of the value, its best to use a useEffect to log the value, which will occur during the rendering phase of React once setState is called
useEffect(() => {
console.log(count); //this will always show the latest state in the console, since it reacts to a change in count after the asynchronous call of setState.
},[count])
You need to use a parameter inside the useEffect hook and re-render only if some changes are made. Below is an example with the count variable and the hook re-render only if the count values ​​have changed.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
The problem is that await api.get() will return a promise so the constant data is not going to have it's data set when the line setUserQueues(queues); is run.
You should do:
api.get(`/users/${userId}`).then(data=>{
setUser((prevState) => {
return { ...prevState, ...data };
});
const queues = data.queues.map((q) => ({
value: q.id,
label: q.name,
}));
setUserQueues(queues);
console.log(queues);
console.log(userQueues);});

Communication Parent and Child component with useEffect

I have issues with communication between a parent and a child component.
I would like the parent (Host) to hold his own state. I would like the child (Guest) to be passed that state and modify it. The child has his local version of the state which can change however the child wants. However, once the child finishes playing with the state, he passes it up to the parent to actually "Save" the actual state.
How would I correctly implement this?
Issues from my code:
on the updateGlobalData handler, I log both data and newDataFromGuest and they are the same. I would like data to represent the old version of the data, and newDataFromGuest to represent the new
updateGlobalData is being called 2X. I can solve this by removing the updateGlobalData ref from the deps array inside useEffect but I don't want to heck it.
My desired results should be:
the data state should hold the old data until updateGlobalData is called
I want updateGlobalData to be fired only once when I click the button
Code from Codesandbox:
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const Host = () => {
const [data, setData] = useState({ foo: { bar: 1 } });
const updateGlobalData = newDataFromGuest => {
console.log(data);
console.log(newDataFromGuest);
setData(newDataFromGuest);
};
return <Guest data={data} updateGlobalData={updateGlobalData} />;
};
const Guest = ({ data, updateGlobalData }) => {
const [localData, setLocalData] = useState(data);
const changeLocalData = newBarNumber => {
localData.foo = { bar: newBarNumber };
setLocalData({ ...localData });
};
useEffect(() => {
updateGlobalData(localData);
}, [localData, updateGlobalData]);
return (
<div>
<span>{localData.foo.bar}</span> <br />
<button onClick={() => changeLocalData(++localData.foo.bar)}>
Increment
</button>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Host />, rootElement);
NOTE: Code solution below
Problem 1:
I want updateGlobalData to be fired only once when I click the button
To solve this issue, I have used a mix between React.createContext and the hook useReducer. The idea is to make the Host dispatcher available through its context. This way, you do not need to send the "updateGlobalData" callback down to the Guest, nor make the useEffect hook to be dependant of it. Thus, useEffect will be triggered only once.
Note though, that useEffect now depends on the host dipatcher and you need to include it on its dependencies. Nevertheless, if you read the first note on useReducer, a dispatcher is stable and will not cause a re-render.
Problem 2:
the data state should hold the old data until updateGlobalData is called
The solution is easy: DO NOT CHANGE STATE DATA DIRECTLY!! Remember that most values in Javascript are passed by reference. If you send data to the Guest and you directly modify it, like here
const changeLocalData = newBarNumber => {
localData.foo = { bar: newBarNumber }; // YOU ARE MODIFYING STATE DIRECTLY!!!
...
};
and here
<button onClick={() => changeLocalData(++localData.foo.bar)}> // ++ OPERATOR MODIFYES STATE DIRECLTY
they will also be modified in the Host, unless you change that data through the useState hook. I think (not 100% sure) this is because localData in Guest is initialized with the same reference as data coming from Host. So, if you change it DIRECTLY in Guest, it will also be changed in Host. Just add 1 to the value of your local data in order to update the Guest state, without using the ++ operator. Like this:
localData.foo.bar + 1
This is my solution:
import React, { useState, useEffect, useReducer, useContext } from "react";
import ReactDOM from "react-dom";
const HostContext = React.createContext(null);
function hostReducer(state, action) {
switch (action.type) {
case "setState":
console.log("previous Host data value", state);
console.log("new Host data value", action.payload);
return action.payload;
default:
throw new Error();
}
}
const Host = () => {
// const [data, setData] = useState({ foo: { bar: 1 } });
// Note: `dispatch` won't change between re-renders
const [data, dispatch] = useReducer(hostReducer, { foo: { bar: 1 } });
// const updateGlobalData = newDataFromGuest => {
// console.log(data.foo.bar);
// console.log(newDataFromGuest.foo.bar);
// setData(newDataFromGuest);
// };
return (
<HostContext.Provider value={dispatch}>
<Guest data={data} /*updateGlobalData={updateGlobalData}*/ />
</HostContext.Provider>
);
};
const Guest = ({ data /*, updateGlobalData*/ }) => {
// If we want to perform an action, we can get dispatch from context.
const hostDispatch = useContext(HostContext);
const [localData, setLocalData] = useState(data);
const changeLocalData = newBarNumber => {
// localData.foo = { bar: newBarNumber };
// setLocalData({ ...localData });
setLocalData({ foo: { bar: newBarNumber } });
};
useEffect(() => {
console.log("useEffect", localData);
hostDispatch({ type: "setState", payload: localData });
// updateGlobalData(localData);
}, [localData, hostDispatch /*, updateGlobalData*/]);
return (
<div>
<span>{localData.foo.bar}</span> <br />
<button onClick={() => changeLocalData(localData.foo.bar + 1)}>
Increment
</button>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Host />, rootElement);
If you see anything not matching with what you want, please, let me know and I will re-check it.
I hope it helps.
Best,
Max.

Calling 'setState' of hook within context sequentially to store data resulting in race condition issues

I've created a context to store values of certain components for display elsewhere within the app.
I originally had a single display component which would use state when these source components were activated, but this resulted in slow render times as the component was re-rendered with the new state every time the selected component changed.
To resolve this I thought to create an individual component for each source component and render them with initial values and only re-render when the source components values change.
i.e. for the sake of an example
const Source = (props) => {
const { name, some_data} = props;
const [setDataSource] = useContext(DataContext);
useEffect(() => {
setDataSource(name, some_data)
}, [some_data]);
return (
...
);
}
const DataContextProvider = (props) => {
const [currentState, setState] = useState({});
const setDataSource = (name, data) => {
const state = {
...currentState,
[name]: {
...data
}
}
}
return (
...
)
}
// In application
<Source name="A" data={{
someKey: 0
}}/>
<Source name="B" data={{
someKey: 1
}}/>
The state of my provider will look like so;
{
"B": {
"someKey": 1
}
}
I believe this is because setState is asynchronous, but I can't think of any other solution to this problem
You can pass the function to setState callback:
setState((state) => ({...state, [name]: data}))
It takes the latest state in argument in any case, so it always safer to use if your update depends on previous state.

Resources