TL;DR
Why there is no dependency array for useState(), something like:
const [state, setState] = useState<T>(initialState, [initialState]);
Question
In React I often end up in this situation
export function EditComponent<T>(props: {
initialState: T,
onSave: (value: T) => void,
}) {
const { initialState, onSave } = props;
const [state, setState] = useState<T>(initialState);
useEffect(() => setState(initialState), [initialState]);
function revert() {
setState(initialState);
}
function save() {
onSave(state);
}
return (
...
)
}
Where I have an outer component that provides some data and an inner component that lets the user edit it (by modifying a copy of the data) to then eventually save or revert. Of course, I need the inner component to be reactive to any outer change (maybe new data has been fetched from the network or whatever).
What I dislike is doing this:
const [state, setState] = useState<T>(initialState);
useEffect(() => setState(initialState), [initialState]);
Cause from my understanding of React this is rendering the component twice when initialState changes:
First to render its parent's children (due to initialState change). In this cycle, the useEffect update is added to the queue to be performed after rendering.
and then a second type after the useEffect update has been performed.
What I need is a dependency array on useState, to do:
const [state, setState] = useState<T>(initialState, [initialState]);
It seems something straightforward, the same as initialState is synchronously consumed, and made available, during the first rendering cycle, when the dependency list changes this operation shall be performed again.
I attempted to implement it myself, but what I came up with seems more like a hack:
import { DependencyList, Dispatch, SetStateAction, useRef, useState } from 'react';
interface MemoContext<S> {
deps: DependencyList | undefined;
state?: S
}
// Is dependency list equal (L327 areHookInputsEqual)
function areHookInputsEqual(a: DependencyList | undefined, b: DependencyList | undefined): boolean {
if (!a) {
console.error('Prev deps should not be null')
return false;
} else if (!b) {
return false;
}
for (let i = 0; i < a.length && i < b.length; i++) {
if (!Object.is(a[i], b[i])) {
return false;
}
}
return true;
}
export function useMemoState<S>(
initialState: S | (() => S),
deps?: DependencyList,
): [S, Dispatch<SetStateAction<S>>] {
function resetInitialState() {
const s: S = typeof initialState === 'function' ? (initialState as any)() : initialState;
ctx.state = s;
ctx.deps = deps;
return s;
}
const ctx = useRef<MemoContext<S>>({ deps: undefined, state: undefined }).current;
// this is actually used just to preserve the rendering behaviour
const [state, setState] = useState<S>(resetInitialState);
if (!areHookInputsEqual(ctx.deps, deps)) {
// They are different, perform the update
resetInitialState()
}
function dispatch(action: SetStateAction<S>) {
setState(prevState => {
const s: S = typeof action === 'function' ? (action as any)(prevState) : action;
ctx.state = s;
ctx.deps = deps;
return s;
})
}
return [ctx.state!, dispatch];
}
To be honest React Core seems like something that should not be touched. So I'm wondering if I'm missing something and if there is a clear reason why such a feature does not exist. Or maybe there is a better solution to this?
A short answer - we do not need it=)
This is your refactored code snippet:
export function EditComponent<T>(props: {
initialState: T,
onSave: (value: T) => void,
}) {
const { initialState, onSave } = props;
const [state, setState] = useState<T>(initialState);
if (initialState !== state){
setState(initialState);
}
function revert() {
setState(initialState);
}
function save() {
onSave(state);
}
return (
...
)
}
Just conditionally update state during rendering.
There are 2 links which you may find helpful:
https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
Related
I have a context store and it is working fine .
I wrote a custom hook to use that context more easily
here is the code :
import { useState, useCallback } from 'react';
import { useStore, useActions, SET } from 'context';
const useContextStore = (key: string): [object | any, Function] => {
const store = useStore();
const action = useActions();
const defaultValues = '';
const [data, setData] = useState<object>(() => {
if (store) {
return store[key];
} else {
return defaultValues;
}
});
const storeData: Function = useCallback(
(payload: object) => {
action({ type: SET, path: key, payload: payload });
setData((prev) => ({ ...prev, ...payload }));
},
[action, key]
);
return [data, storeData];
};
export { useContextStore };
it is working fine but when I use This hook in other component they dont rerender when new store is set
What I have Tried : was to replace code
return [data, storeData];
with
return [store?.[key], storeData];
and My Problem solved but I realy have no any Idea Why This is Happening ...
I Had similar problem with my custom useLocalStorage hook and I am so frustrated about spotting the problem.
I deleted useCallback and my problem solved but I have no clue as to Why ?
When you are doing ...prev, it should be throwing error as you can not spread string. Your defaultValue is a string.
Your code only works when data is an object, which may not be the case.
Also, try changing how you are initializing the state.
const [data, setData] = useState<object>(() => {
if (store) {
return store[key];
} else {
return defaultValues;
}
});
to
const [data, setData] = useState<object>(store?.[key] || defaultValues)
Couldn't find any related answer, the only one was related to Redux directly, thus asking a question that may seem obvious to some of you.
As far as my code it seems that everything is correct yet I'm struggling with the following error: Expected 0 arguments, but got 1
Code:
// An enum with all the types of actions to use in our reducer
enum ActionType {
INCREASE = 'INCREASE',
}
// An interface for our actions
interface Action {
types: ActionType;
payload: number;
}
// An interface for our state
interface allState {
playersNumber: number,
playerData: string
}
const reducer : any = (state : allState, action : Action) => {
switch (action.types) {
case ActionType.INCREASE:
return { playersNumber: state.playersNumber + 5}
}
}
const initialState = {
playersNumber : 0,
playerData: ""
}
const App : FC<any> = () => {
const [responseData , setResponseData] = useState<Array<object>>([]);
const [state, dispatch] = useReducer(reducer, initialState);
// Fetching data with Axios
const fetchingData = async () => {
const response = await axios.get<Array<object>>('http://localhost:3006/lfy');
setResponseData(response.data);
console.log(response.data.length);
dispatch({type: ActionType.INCREASE})
}
useEffect(() => {
fetchingData();
}, []);
return (
<Routes>
{console.log(state)}
<Route path="/" element={<Home />} />
</Routes>
);
}
export default App;
You are quite there! There is only a few things to fix. First, you need to remove the type any from the reducer and always return the whole state inside the function:
const reducer = (state : allState, action : Action) => {
switch (action.types) {
case ActionType.INCREASE:
return {...state, playersNumber: state.playersNumber + 5}; // use the shallow copy for the state so you make sure to not replace it completely
default:
return state;
}
}
Ultimately, when you call dispatch you wrote type instead of types and you also did not pass the payload. If the payload is optional you have to set it as optional in the interface:
interface Action {
types: ActionType;
payload?: number; // note the question mark
}
dispatch({types: ActionType.INCREASE});
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.
I've been working with React for a while, and yesterday I got my feet wet with hooks in a Typescript based project. Before refactoring, the class had a state like this:
interface INavItemProps {
route: IRoute;
}
interface INavItemState {
toggleStateOpen: boolean
}
class NavItem extends Component<INavItemProps, INavItemState> {
constructor() {
this.state = { toggleStateOpen: false };
}
public handleClick = (element: React.MouseEvent<HTMLElement>) => {
const toggleState = !this.state.toggleStateOpen;
this.setState({ toggleStateOpen: toggleState });
};
...
}
Now, when refactoring to a functional component, I started out with this
interface INavItemProps {
route: IRoute;
}
const NavItem: React.FunctionComponent<INavItemProps> = props => {
const [toggleState, setToggleState] = useState<boolean>(false);
const { route } = props;
const handleClick = (element: React.MouseEvent<HTMLElement>) => {
const newState = !toggleState;
setToggleState(newState);
};
...
}
But then I also tested this:
interface INavItemProps {
route: IRoute;
}
interface INavItemState {
toggleStateOpen: boolean
}
const NavItem: React.FunctionComponent<INavItemProps> = props => {
const [state, setToggleState] = useState<INavItemState>({toggleStateOpen: false});
const { route } = props;
const handleClick = (element: React.MouseEvent<HTMLElement>) => {
const newState = !state.toggleStateOpen;
setToggleState({toggleStateOpen: newState});
};
...
}
Is there such a thing as a correct way of defining the state in cases like this? Or should I simply just call more hooks for each slice of the state?
useState hook allows for you to define any type of state like an Object, Array, Number, String, Boolean etc. All you need to know is that hooks updater doesn't merge the state on its own unline setState, so if you are maintain an array or an object and you pass in only the value to be updated to the updater, it would essentially result in your other states getting lost.
More often than not it might be best to use multiple hooks instead of using an object with one useState hook or if you want you can write your own custom hook that merges the values like
const useMergerHook = (init) => {
const [state, setState] = useState(init);
const updater = (newState) => {
if (Array.isArray(init)) {
setState(prv => ([
...prv,
...newState
]))
} else if(typeof init === 'object' && init !== null) {
setState(prv => ({
...prv,
...newState
}))
} else {
setState(newState);
}
}
return [state, updater];
}
Or if the state/state updates need to be more complex and the handler need to be passed down to component, I would recommend using useReducer hook since you have have multiple logic to update state and can make use of complex states like nested objects and write logic for the update selectively
I have updated this with an update at the bottom
Is there a way to maintain a monolithic root state (like Redux) with multiple Context API Consumers working on their own part of their Provider value without triggering a re-render on every isolated change?
Having already read through this related question and tried some variations to test out some of the insights provided there, I am still confused about how to avoid re-renders.
Complete code is below and online here: https://codesandbox.io/s/504qzw02nl
The issue is that according to devtools, every component sees an "update" (a re-render), even though SectionB is the only component that sees any render changes and even though b is the only part of the state tree that changes. I've tried this with functional components and with PureComponent and see the same render thrashing.
Because nothing is being passed as props (at the component level) I can't see how to detect or prevent this. In this case, I am passing the entire app state into the provider, but I've also tried passing in fragments of the state tree and see the same problem. Clearly, I am doing something very wrong.
import React, { Component, createContext } from 'react';
const defaultState = {
a: { x: 1, y: 2, z: 3 },
b: { x: 4, y: 5, z: 6 },
incrementBX: () => { }
};
let Context = createContext(defaultState);
class App extends Component {
constructor(...args) {
super(...args);
this.state = {
...defaultState,
incrementBX: this.incrementBX.bind(this)
}
}
incrementBX() {
let { b } = this.state;
let newB = { ...b, x: b.x + 1 };
this.setState({ b: newB });
}
render() {
return (
<Context.Provider value={this.state}>
<SectionA />
<SectionB />
<SectionC />
</Context.Provider>
);
}
}
export default App;
class SectionA extends Component {
render() {
return (<Context.Consumer>{
({ a }) => <div>{a.x}</div>
}</Context.Consumer>);
}
}
class SectionB extends Component {
render() {
return (<Context.Consumer>{
({ b }) => <div>{b.x}</div>
}</Context.Consumer>);
}
}
class SectionC extends Component {
render() {
return (<Context.Consumer>{
({ incrementBX }) => <button onClick={incrementBX}>Increment a x</button>
}</Context.Consumer>);
}
}
Edit: I understand that there may be a bug in the way react-devtools detects or displays re-renders. I've expanded on my code above in a way that displays the problem. I now cannot tell if what I am doing is actually causing re-renders or not. Based on what I've read from Dan Abramov, I think I'm using Provider and Consumer correctly, but I cannot definitively tell if that's true. I welcome any insights.
There are some ways to avoid re-renders, also make your state management "redux-like". I will show you how I've been doing, it far from being a redux, because redux offer so many functionalities that aren't so trivial to implement, like the ability to dispatch actions to any reducer from any actions or the combineReducers and so many others.
Create your reducer
export const initialState = {
...
};
export const reducer = (state, action) => {
...
};
Create your ContextProvider component
export const AppContext = React.createContext({someDefaultValue})
export function ContextProvider(props) {
const [state, dispatch] = useReducer(reducer, initialState)
const context = {
someValue: state.someValue,
someOtherValue: state.someOtherValue,
setSomeValue: input => dispatch('something'),
}
return (
<AppContext.Provider value={context}>
{props.children}
</AppContext.Provider>
);
}
Use your ContextProvider at top level of your App, or where you want it
function App(props) {
...
return(
<AppContext>
...
</AppContext>
)
}
Write components as pure functional component
This way they will only re-render when those specific dependencies update with new values
const MyComponent = React.memo(({
somePropFromContext,
setSomePropFromContext,
otherPropFromContext,
someRegularPropNotFromContext,
}) => {
... // regular component logic
return(
... // regular component return
)
});
Have a function to select props from context (like redux map...)
function select(){
const { someValue, otherValue, setSomeValue } = useContext(AppContext);
return {
somePropFromContext: someValue,
setSomePropFromContext: setSomeValue,
otherPropFromContext: otherValue,
}
}
Write a connectToContext HOC
function connectToContext(WrappedComponent, select){
return function(props){
const selectors = select();
return <WrappedComponent {...selectors} {...props}/>
}
}
Put it all together
import connectToContext from ...
import AppContext from ...
const MyComponent = React.memo(...
...
)
function select(){
...
}
export default connectToContext(MyComponent, select)
Usage
<MyComponent someRegularPropNotFromContext={something} />
//inside MyComponent:
...
<button onClick={input => setSomeValueFromContext(input)}>...
...
Demo that I did on other StackOverflow question
Demo on codesandbox
The re-render avoided
MyComponent will re-render only if the specifics props from context updates with a new value, else it will stay there.
The code inside select will run every time any value from context updates, but it does nothing and is cheap.
Other solutions
I suggest check this out Preventing rerenders with React.memo and useContext hook.
I made a proof of concept on how to benefit from React.Context, but avoid re-rendering children that consume the context object. The solution makes use of React.useRef and CustomEvent. Whenever you change count or lang, only the component consuming the specific proprety gets updated.
Check it out below, or try the CodeSandbox
index.tsx
import * as React from 'react'
import {render} from 'react-dom'
import {CountProvider, useDispatch, useState} from './count-context'
function useConsume(prop: 'lang' | 'count') {
const contextState = useState()
const [state, setState] = React.useState(contextState[prop])
const listener = (e: CustomEvent) => {
if (e.detail && prop in e.detail) {
setState(e.detail[prop])
}
}
React.useEffect(() => {
document.addEventListener('update', listener)
return () => {
document.removeEventListener('update', listener)
}
}, [state])
return state
}
function CountDisplay() {
const count = useConsume('count')
console.log('CountDisplay()', count)
return (
<div>
{`The current count is ${count}`}
<br />
</div>
)
}
function LangDisplay() {
const lang = useConsume('lang')
console.log('LangDisplay()', lang)
return <div>{`The lang count is ${lang}`}</div>
}
function Counter() {
const dispatch = useDispatch()
return (
<button onClick={() => dispatch({type: 'increment'})}>
Increment count
</button>
)
}
function ChangeLang() {
const dispatch = useDispatch()
return <button onClick={() => dispatch({type: 'switch'})}>Switch</button>
}
function App() {
return (
<CountProvider>
<CountDisplay />
<LangDisplay />
<Counter />
<ChangeLang />
</CountProvider>
)
}
const rootElement = document.getElementById('root')
render(<App />, rootElement)
count-context.tsx
import * as React from 'react'
type Action = {type: 'increment'} | {type: 'decrement'} | {type: 'switch'}
type Dispatch = (action: Action) => void
type State = {count: number; lang: string}
type CountProviderProps = {children: React.ReactNode}
const CountStateContext = React.createContext<State | undefined>(undefined)
const CountDispatchContext = React.createContext<Dispatch | undefined>(
undefined,
)
function countReducer(state: State, action: Action) {
switch (action.type) {
case 'increment': {
return {...state, count: state.count + 1}
}
case 'switch': {
return {...state, lang: state.lang === 'en' ? 'ro' : 'en'}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}: CountProviderProps) {
const [state, dispatch] = React.useReducer(countReducer, {
count: 0,
lang: 'en',
})
const stateRef = React.useRef(state)
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {count: state.count},
})
document.dispatchEvent(customEvent)
}, [state.count])
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {lang: state.lang},
})
document.dispatchEvent(customEvent)
}, [state.lang])
return (
<CountStateContext.Provider value={stateRef.current}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
)
}
function useState() {
const context = React.useContext(CountStateContext)
if (context === undefined) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
function useDispatch() {
const context = React.useContext(CountDispatchContext)
if (context === undefined) {
throw new Error('useDispatch must be used within a AccountProvider')
}
return context
}
export {CountProvider, useState, useDispatch}
To my understanding, the context API is not meant to avoid re-render but is more like Redux. If you wish to avoid re-render, perhaps looks into PureComponent or lifecycle hook shouldComponentUpdate.
Here is a great link to improve performance, you can apply the same to the context API too