Execution order between React's useEffect and DOM event handler - javascript

I wrote a custom context provider in the below which holds application settings as a Context as well as save it in localStorage (thanks to a post by Alex Krush).
I added initialized flag to avoid saving the value fetched from localStorage right after the component is mounted (useEffect will be run at the timing of componentDidMount and try to write the fetched value to the storage).
import React, { useCallback, useEffect, useReducer, useRef } from 'react';
const storageKey = 'name';
const defaultValue = 'John Doe';
const initializer = (initialValue) => localStorage.getItem(storageKey) || initialValue;
const reducer = (value, newValue) => newValue;
const CachedContext = React.createContext();
const CachedContextProvider = (props) => {
const [value, setValue] = useReducer(reducer, defaultValue, initializer);
const initialized = useRef(false);
// save the updated value as a side-effect
useEffect(() => {
if (initialized.current) {
localStorage.setItem(storageKey, value);
} else {
initialized.current = true; // skip saving for the first time
}
}, [value]);
return (
<CachedContext.Provider value={[value, setValue]}>
{props.children}
</CachedContext.Provider>
);
};
Usage:
const App = (props) => {
return <CachedContextProvider><Name name='Jane Doe' /></CachedContextProvider>;
}
const Name = (props) => {
const [name, setName] = useContext(CachedContext);
useEffect(() => {
setName(props.name);
}, [props.name]);
}
Then, I'd like to make my custom context detect changes to the target storage made by another window. I added handleStorageEvent to CachedContextProvider for listening storage events:
// re-initialize when the storage has been modified by another window
const handleStorageEvent = useCallback((e) => {
if (e.key === storageKey) {
initialized.current = false; // <-- is it safe???
setValue(initializer(defaultValue));
}
}, []);
useEffect(() => {
if (typeof window !== 'undefined') {
window.addEventListener('storage', handleStorageEvent);
return () => {
window.removeEventListener('storage', handleStorageEvent);
};
}
}, []);
My concern is whether I can reset initialized to false safely for avoid writing back the fetched value. I'm worried about the following case in the multi-process setting:
Window 1 runs setValue('Harry Potter')
Window 2 runs setValue('Harry Potter')
Window 2 runs localStorage.setItem in response to update on value
handleStorageEvent in Window 1 detects the change in storage and re-initialize its initialized and value as false and 'Harry Potter'
Window 1 try to run localStorage.setItem, but it does nothing because value is already set as 'Harry Potter' by Window 2 and React may judge there is no changes. As a result, initialized will be kept as false
Window 1 runs setValue('Ron Weasley'). It updates value but does not save it because initialized === false. It has a chance to lose the value set by the application
I think it is related to the execution order between React's useEffect and DOM event handler. Does anyone know how to do it right?

I would probably add some sort of test to see what happens in every possible scenario.
However, this is my theory: In step 5, window 1 will not try to run localStorage.setItem (as you said), since initialized was just set to false. It will instead set initialized to true. Step 6 should therefore work as expected, and this shouldn't be an issue.

I discussed the problem with my colleague and finally found a solution. He pointed out that new React Fiber engine should not ensure the order of execution of side-effects, and suggested adding a revision number to the state.
Here is an example. The incremented revision will always invoke useEffect even if the set value is not changed. Subscribers obtain state.value from Provider and don't need to concern about the underlying revision.
import React, { useCallback, useEffect, useReducer, useRef } from 'react';
const storageKey = 'name';
const defaultValue = 'John Doe';
const orDefault(value) = (value) =>
(typeof value !== 'undefined' && value !== null) ? value : defaultValue;
const initializer = (arg) => ({
value: orDefault(localStorage.getItem(storageKey)),
revision: 0,
});
const reducer = (state, newValue) => ({
value: newValue,
revision: state.revision + 1,
});
const useCachedValue = () => {
const [state, setState] = useReducer(reducer, null, initializer);
const initialized = useRef(false);
// save the updated value as a side-effect
useEffect(() => {
if (initialized.current) {
localStorage.setItem(storageKey, state.value);
} else {
// skip saving just after the (re-)initialization
initialized.current = true;
}
}, [state]);
// re-initialize when the storage has been modified by another window
const handleStorageEvent = useCallback((e) => {
if (e.key === null || e.key === storageKey) {
initialized.current = false;
setState(orDefault(e.newValue));
}
}, []);
useEffect(() => {
if (typeof window !== 'undefined') {
window.addEventListener('storage', handleStorageEvent);
return () => {
window.removeEventListener('storage', handleStorageEvent);
};
}
}, []);
return [state.value, setState];
};
const Context = React.createContext();
const Provider = (props) => {
const cachedValue = useCachedValue();
return (
<Context.Provider value={cachedValue}>
{props.children}
</Context.Provider>
);
};

Related

How to stop initial render for useEffect hook

Earlier I had a Class component, so I didn't face any issues while using lifecycle methods, but after converting to useEffect hooks, I am facing the initial render issue which I don't want to happen.
Class
componentDidMount() {
this.setState({
patchVal:this.props.patchTaskVal,
startTime:this.props.patchStartTime,
setEndTime:this.props.patchEndTime
})
}
componentDidUpdate(prevProps) {
if (prevProps.patchTaskVal !== this.props.patchTaskVal) {
this.callValidation()
}
if (prevProps.closeTask !== this.props.closeTask) {
this.setState({
showValue:false,
progressValue:[],
startTime:new Date(),
setEndTime:""
})
}
if (prevProps.patchStartTime !== this.props.patchStartTime || prevProps.endTime !== this.props.endTime && this.props.endTime !== "") {
this.setState({
startTime:this.props.patchStartTime,
setEndTime:parseInt(this.props.endTime)
})
}
}
Functional
const [patchTaskVal, setPatchTaskVal]=useState(/*initial value */)
const [startTime, setStartTime]=useState()
const [endTime, setEndTime] = useState()
**// I want only this useEffect to run on the initial render**
useEffect(() => {
setPatchTaskVal(props.patchTaskVal)
...//set other states
}, [])
useEffect(() => {
callValidation()
}, [props.patchTaskVal])
useEffect(() => {
//setShowValue...
}, [props.closeTask])
useEffect(() => {
if (props.endTime != "") {
// set states...
}
}, [props.patchStartTime,props.endTime])
Here I am facing an issue where all the useEffects are running on the initial render, Please suggest a solution for this so that only the first useEffect will run on the initial render and all other useEffects will run according to its dependency prop values.
You basically need a ref which will tell you whether this is the first render on not. Refs values persist over rerenders. You can start with a truthy value and toggle it to false after the first render (using a useEffect with an empty array[]). Based on that you can run your desired code.
You can also put the whole thing in a custom hook:
import { useEffect, useRef } from "react";
const useOnUpdate = (callback, deps) => {
const isFirst = useRef(true);
useEffect(() => {
if (!isFirst.current) {
callback();
}
}, deps);
useEffect(() => {
isFirst.current = false;
}, []);
};
export default useOnUpdate;
You can call this hook in your component like :
useOnUpdate(() => {
console.log(prop);
}, [prop]);
In the hook:
After the initial render, both useEffects run. But when the first effect runs the value of the isFirst.current is true. So the callback is not called. The second useEffect also runs and sets isFirst.current to false.
Now in subsequent renders only the first useEffect run (when dependencies change), and isFirst.current is false now so callback is executed.
The order of the two useEffects is very important here. Otherwise, in the useEffect with deps, isFirst.current will be true even after the first render.
Link
If you compare the functional and the class component you can notice that there is one part missing - previous props.
Functional component does not have previous props in scope, but you can save them yourself with a small trick: save them to reference so it will not impact you render cycle.
Since now you have the previous props and the current props you can apply the same logic you did for class component.
import React, { useRef, useEffect, useState } from "react";
default function App() {
const [input, setInput] = useState("");
const [commitInput, setCommitInput] = useState("");
return (
<>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={() => setCommitInput(input)}>apply</button>
<Child test={commitInput} />
</>
);
}
function Child(props) {
const prev = useRef(props.test);
useEffect(() => {
if (prev.current !== props.test) {
alert("only when changes");
}
}, [props.test]);
return <div>{props.test}</div>;
}
try this...
let init = true;
useEffect( ()=>{
if(init) {
setPatchTaskVal(props.patchTaskVal)
init = false;
...//set other states}
}, [])
useEffect( ()=> {
!init && callValidation()
},[props.patchTaskVal])
useEffect( ()=>{
//!init && setShowValue...
},[props.closeTask])
useEffect( ()=>{
if(props.endTime!="" && !init){
// set states...
}
},[props.patchStartTime,props.endTime])
Hope my understanding is right about your question.
Why not just add a if statement to check the state is not undefined or default value
useEffect( ()=> {
if (props.patchTaskVal) {
callValidation()
}
},[props.patchTaskVal])
useEffect( ()=>{
if (props.closeTask) {
//setShowValue...
}
},[props.closeTask])
useEffect( ()=>{
if(props.patchStartTime){
// set states...
}
if(props.endTime){
// set states...
}
},[props.patchStartTime,props.endTime]
And according your class component,
this.setState({
patchVal:this.props.patchTaskVal,
startTime:this.props.patchStartTime,
setEndTime:this.props.patchEndTime
})
The function component should map props to component's state. Like this
const [patchTaskVal, setPatchTaskVal]=useState(props.patchTaskVal)
const [startTime, setStartTime]=useState(props.patchStartTime)
const [endTime, setEndTime] = useState(props.patchEndTime)

useMemo for efficient global data availability using reactJS and recoilJS

I am trying to figure out how to solve the following problem in the best way possible:
I have multiple components all requiring a global state (I am using recoil for this, since I have many different "atom" states).
Only if a component gets loaded that requires that state, it will perform an expensive computation that fetches the data. This should happen only once upon initialisation. Other components that require the same piece of data should not re-trigger the data fetching, unless they explicitly call an updateState function.
Ideally, my implementation would look something like this:
const initialState = {
uri: '',
balance: '0',
};
const fetchExpensiveState = () => {
uri: await fetchURI(),
balance: await fetchBalance(),
});
const MyExpensiveData = atom({
key: 'expensiveData',
default: initialState,
updateState: fetchExpensiveState,
});
function Component1() {
const data = useRecoilMemo(MyExpensiveData); // triggers `fetchExpensiveState` upon first call
return ...
}
function Component2() {
const data = useRecoilMemo(MyExpensiveData); // doesn't trigger `fetchExpensiveState` a second time
return ...
}
I could solve this by using useRecoilState and additional variables in the context that tell me whether this has been initialised already, like so:
export function useExpensiveState() {
const [context, setContext] = useRecoilState(MyExpensiveData);
const updateState = useCallback(async () => {
setContext({...fetchExpensiveState(), loaded: true});
}, []);
useEffect(() => {
if (!context.loaded) {
setContext({...context, loaded: true});
updateState();
}
}, []);
return { ...context, updateState };
}
It would be possible to make this solution more elegant (not mixing loaded with the state for example). Although, because this should be imo essential and basic, it seems as though I'm missing some solution that I haven't come across yet.
I solved it first by using a loaded and loading state using more useRecoilStates. However, when mounting components, that had other components as children, that all used the same state, I realized that using recoil's state would not work, since the update is only performed on the next tick. Thus, I chose to use globally scoped dictionaries instead (which might not look pretty, but works perfectly fine for this use case).
Full code, in case anyone stumbles upon a problem like this.
useContractState.js
import { useWeb3React } from '#web3-react/core';
import { useEffect, useState } from 'react';
import { atomFamily, useRecoilState } from 'recoil';
const contractState = atomFamily({
key: 'ContractState',
default: {},
});
var contractStateInitialized = {};
var contractStateLoading = {};
export function useContractState(key, fetchState, initialState, initializer) {
const [state, setState] = useRecoilState(contractState(key));
const [componentDidMount, setComponentMounting] = useState(false);
const { library } = useWeb3React();
const provider = library?.provider;
const updateState = () => {
fetchState()
.then(setState)
.then(() => {
contractStateInitialized[key] = true;
contractStateLoading[key] = false;
});
};
useEffect(() => {
// ensures that this will only be called once per init or per provider update
// doesn't re-trigger when a component re-mounts
if (provider != undefined && !contractStateLoading[key] && (componentDidMount || !contractStateInitialized[key])) {
console.log('running expensive fetch:', key);
contractStateLoading[key] = true;
if (initializer != undefined) initializer();
updateState();
setComponentMounting(true);
}
}, [provider]);
if (!contractStateInitialized[key] && initialState != undefined) return [initialState, updateState];
return [state, updateState];
}
useSerumContext.js
import { useSerumContract } from '../lib/ContractConnector';
import { useContractState } from './useContractState';
export function useSerumContext() {
const { contract } = useSerumContract();
const fetchState = async () => ({
owner: await contract.owner(),
claimActive: await contract.claimActive(),
});
return useContractState('serumContext', fetchState);
}
The reason why I have so many extra checks is that I don't want to re-fetch the state when the component re-mounts, but the state has already been initialised. The state should however subscribe to updates on provider changes and re-fetch if it has changed.

setState in useEffect not working in conjunction with localStorage

This seems unordinary. I'm attempting to simply setState on mount, but it's just going blank on me.
const [info, setState] = useState({
user_1: '',
user_2: '',
});
useEffect(() => {
var personal_1 = window.localStorage.getItem('personal_1');
var personal_2 = window.localStorage.getItem('personal_2');
console.log(personal_1);
if (personal_1 !== null && personal_2 !== null) {
console.log('Make it easy')
setState({
...info,
user_1: personal_1,
user_2: personal_2
})
}
}, [])
useEffect(() => {
console.log('User personal_1 was changed to', info.user_1);
}, [info.user_1])
When I log the effects of this component mounting,
it shows
personal_1
Make it easy
User personal_1 was changed to
For some reason instead of setting the state on mount to the value from local storage, it just goes blank. I thought it was probably being set back to being blank from somewhere else in the component, but the info.personal_1 hook only gets called once. Does anyone know why it might potentially be setting the local storage item, but then going blank when the storage item is a normal string?
This is happening because you are not successfully setting the local storage values. Hence, the value of those items will always be blank
You can straight out use the custom React hook (useLocalStorage) I am providing below to get and set items from your localStorage into your web-app
Hook
import { useCallback, useState, useEffect } from "react"
// To use local storage to store, update info
export function useLocalStorage(key, defaultValue) {
return useStorage(key, defaultValue, window.localStorage)
}
// Common code being used irresp of storage type -> local or session
function useStorage(key, defaultValue, storageObject) {
// Set the "value" (in this hook's state, and return it to the component's state as well) to be the value at passed "key" in storage.
// Else, we set it to the defaultValue (which can be a value or a func returning a val)
const [value, setValue] = useState(() => {
const jsonValue = storageObject.getItem(key)
if (jsonValue != null) return JSON.parse(jsonValue)
if (typeof defaultValue === "function") {
return defaultValue()
} else {
return defaultValue
}
})
// To set a value in the storage using key-val pair
useEffect(() => {
if (value === undefined) return storageObject.removeItem(key)
storageObject.setItem(key, JSON.stringify(value))
}, [key, value, storageObject])
// To remove value at given key
const remove = useCallback(() => {
setValue(undefined)
}, [])
return [value, setValue, remove]
}
Using the hook in a component :
import { useLocalStorage } from "./useStorage"
export default function StorageComponent() {
const [age, setAge, removeAge] = useLocalStorage("age", 26)
return (
<div>
<div>
{age}
</div>
<button onClick={() => setAge(40)}>Set Age</button>
<button onClick={removeAge}>Remove Age</button>
</div>
)
}
in second useEffect dependency just add info like this:
useEffect(() => {
if(info.user_1){
console.log('User personal_1 was
changedto',info.user_1);
}
}, [info])

Function passed as parameter to custom hooks is not taking new value of variables

I use below custom hook for listening to window unload event.
import { useRef, useEffect } from 'react';
const useUnload = fn => {
const cb = useRef(fn); // init with fn, so that type checkers won't assume that current might be undefined
useEffect(() => {
cb.current = fn;
}, [fn]);
useEffect(() => {
const onUnload = cb.current;
window.addEventListener("beforeunload", onUnload);
return () => window.removeEventListener("beforeunload", onUnload);
}, []);
};
export default useUnload;
When the unload function is called, the variable are of the values when the hook is initialized. The updated values of variables are not reflected.
useUnload(e => {
console.log(variableA) //It is retaining the old value when the hook is initialized.
e.preventDefault();
e.returnValue = ""
});
The first function will be used because you are dereferencing the current property of the cb ref directly in the effect, which only happens once after the initial render of the component that uses the useUnload hook.
You can create a new function in the effect that in turn dereferences the current property of the cb ref when it is called and the latest function will be used.
const useUnload = fn => {
const cb = useRef(fn); // init with fn, so that type checkers won't assume that current might be undefined
useEffect(() => {
cb.current = fn;
}, [fn]);
useEffect(() => {
const onUnload = () => {
cb.current();
};
window.addEventListener("beforeunload", onUnload);
return () => window.removeEventListener("beforeunload", onUnload);
}, []);
};

react: functional component that ignores prop changes until callback is triggered

I have a controlled component that I want to use to trigger an optimistic update, and, while the update is being performed, it should use it's internal state and ignore all changes to it's main prop, and once a callback is executed then it can again use the provided prop as values.
I tried to encode this logic on a hook using useMemo, useRef, and different setStates but without success. I think it is because the used ref does not survive long enough (the component is re-rendered like 3 times between the update trigger and the callback execution) and at some point it gets re-created with the new value coming from props.
Here is the hook that I done so far:
function useOptimistic(incoming) {
const [state, setState] = useState('INITIAL')
const ref = useRef(incoming)
const value = useMemo(() => ref.current, [state])
const setUpdating = (value) => {
setState('UPDATING')
ref.current = value
}
const setUpdated = () => setState('UPDATED')
return { state: value, setUpdating, setUpdated }
}
The reason is because I am using graphql and a toggle component. When I click on the toggle component I trigger the graphql request and you can see the change with the local state, but while the query is being executed the component is re-rendered 3 or 4 times, and the toggle shows the state from the "old-data", when the request completes then it comes back to the correct state. What I want is to avoid this intermediary weird changes.
I think that this should do it:
const initialState = { updating: false }
function useOptimistic(incoming) {
const [innerState, setInnerState] = useState(initialState)
const setUpdating = (value) => setInnerState({ updating: true, value })
const setUpdated = () => setInnerState(initialState)
const state = innerState.updating ? innerState.value : incoming
return { state, setUpdating, setUpdated }
}
Live Example (based on T.J. Crowder's answer), just to show that it's not really necessary to have stable functions for something like this ;-).
const { useState, useRef } = React;
const initialState = { updating: false }
function useOptimistic(incoming) {
const [innerState, setInnerState] = useState(initialState)
const setUpdating = (value) => setInnerState({ updating: true, value })
const setUpdated = () => setInnerState(initialState)
const state = innerState.updating ? innerState.value : incoming
return { state, setUpdating, setUpdated }
}
function Example() {
const [value, setValue] = useState(1);
const {state, setUpdating, setUpdated} = useOptimistic(value);
const triggerUpdate = () => {
const newValue = value + 1;
setUpdating(newValue);
setTimeout(() => {
setValue(newValue);
setUpdated(); // <=== Note that despite the fact that this function
// is not stable, this still works perfectly fine ;-)
}, 1000);
};
return (
<div>
<div>value: {value}</div>
<div>state: {state}</div>
<input type="button" onClick={triggerUpdate} value="Trigger Update" />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>
A ref will only be created once, when the component using the hook is first mounted, so you were right to try to store the pending state in a ref (although it could also be stored in state). You'd also probably be best off putting the state setters in a ref as well, since making them stable (like useState does) is useful to consumers of the hook, so I'll use the ref for the pending value as well.
The main problem with your hook, I think, is that it always uses the value from ref.current, but it only sets ref.current to incoming when the component using the hook is first mounted. So it ignores subsequent changes to incoming (whether or not it's in the updating state).
The change I'd make (other than using a boolean rather than strings for the flag) is to use your "updating" flag to choose what to return as state, returning the then-current incoming when not updating, or the optimistic pending value if updating. See comments:
function useOptimistic(incoming) {
// A flag for whether we're in the "udating" state
const [updatingFlag, setUpdatingFlag] = useState(false)
// Our instance data
const instance = useRef(null);
if (!instance.current) {
// Only happens on first call, this object is created once
instance.current = {
// The pending value
pendingValue: null,
// Set the pending value and go into the updating state
setUpdating: (value) => {
instance.current.pendingValue = value;
setUpdatingFlag(true);
},
// Exit the updating state
setUpdated: () => {
setUpdatingFlag(false);
instance.current.pendingValue = null;
},
};
}
// The state value we'll return: the input prop, or the one from our
// instance data
const state = updatingFlag ? instance.current.pendingValue : incoming;
// Grab our setters and return them with the state
const { setUpdating, setUpdated } = instance.current;
return { state, setUpdating, setUpdated };
}
Live Example:
const { useState, useRef } = React;
function useOptimistic(incoming) {
// A flag for whether we're in the "udating" state
const [updatingFlag, setUpdatingFlag] = useState(false)
// Our instance data
const instance = useRef(null);
if (!instance.current) {
// Only happens on first call, this object is created once
instance.current = {
// The pending value
pendingValue: null,
// Set the pending value and go into the updating state
setUpdating: (value) => {
instance.current.pendingValue = value;
setUpdatingFlag(true);
},
// Exit the updating state
setUpdated: () => {
setUpdatingFlag(false);
instance.current.pendingValue = null;
},
};
}
// The state value we'll return: the input prop, or the one from our
// instance data
const state = updatingFlag ? instance.current.pendingValue : incoming;
// Grab our setters and return them with the state
const { setUpdating, setUpdated } = instance.current;
return { state, setUpdating, setUpdated };
}
function Example() {
const [value, setValue] = useState(1);
const {state, setUpdating, setUpdated} = useOptimistic(value);
const triggerUpdate = () => {
const newValue = value + 1;
setUpdating(newValue);
setTimeout(() => {
setValue(newValue);
setUpdated(); // <=== Note that the stability of the setters is what
// lets me use this here, it wouldn't be correct if
// the setters weren't stable
}, 1000);
};
return (
<div>
<div>value: {value}</div>
<div>state: {state}</div>
<input type="button" onClick={triggerUpdate} value="Trigger Update" />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

Categories