I am trying to wrap my head around functional programming and one of the main concepts is closures - but I think there is something more that I am lacking and I cannot put things together. This is a state closure that I wrote similar to the one in React, I know the right answer to make the code snippet work, I just don't understand because of what theoretical facets this does not work.
const useState = function () {
let state: maze = {
size: 0,
};
const setState = (newState) => {
state = { ...newState };
};
const myStateObj = {
state,
setState,
};
return myStateObj;
};
const handleMazeSize = (e: InputEvent) => {
const newMaze: maze = {
size: Number((e.target as HTMLInputElement).value),
};
console.log(useState().state);
console.log(useState().setState(newMaze));
console.log(useState().state); // still size 0, expected the inputted size
};
Why does it not get modified?
The two main issues in your snippet are:
Every time you handle the change event, you create a new state with value 0. You need to persist your state outside of your event handler.
The way you expose the state property only returns a reference to the initial internal state object.
There are many ways to solve these issues. In the snippet below I:
Replaced the size property of your useState return object with a getter
Call useState once before attaching the event listener
const useState = function() {
let state = {
size: 0,
};
const setState = (newState) => {
state = { ...newState };
};
const myStateObj = {
get state() { return state; },
setState,
};
return myStateObj;
};
const mazeState = useState();
const handleMazeSize = (e) => {
const newMaze = {
size: Number(e.target.value)
};
console.log("from:", mazeState.state);
mazeState.setState(newMaze);
console.log("to:", mazeState.state);
};
document.querySelector("input").addEventListener("change", handleMazeSize);
<input type="number" value="0">
Here's another way of solving the second issue. Instead of using a get that always returns the current internal value of the let state variable, we expose that state object once and mutate it.
The downside is that you could (accidentally) edit the internal state from the outside. (e.g. const { state } = useState(); state.foo = "bar")
const useState = function() {
const state = {
size: 0,
};
const setState = (newState) => {
// Mutate the exposed object
Object.assign(state, newState);
};
const myStateObj = {
state,
setState,
};
return myStateObj;
};
const mazeState = useState();
const handleMazeSize = (e) => {
const newMaze = {
size: Number(e.target.value)
};
console.log("from:", mazeState.state);
mazeState.setState(newMaze);
console.log("to:", mazeState.state);
};
document.querySelector("input").addEventListener("change", handleMazeSize);
<input type="number" value="0">
Related
Trying to write a custom implementation of useState. Let's say only for a single value.
function useMyState(initVal){
const obj = {
value: initVal,
get stateValGet() {
return this.value
},
set stateValSet(val) {
this.value = val
}
};
const setVal = (val) => {
obj.stateValSet = val
}
return [obj.stateValGet, setVal]
}
Doesn't seem to work though, can anyone tell why?
Unable to crack this.
It returns this [, <function_setter>]
So if you try to run this setVal method, it does trigger the setter. But getter never gets called upon the updation.
useState's functionality can't really be polyfilled or substituted with your own custom implementation, because it not only stores state, but it also triggers a component re-render when the state setter is called. Triggering such a re-render is only possible with access to React internals, which the surface API available to us doesn't have access to.
useState can't be replaced with your own implementation unless that implementation also uses useState itself in order to get the component it's used in to re-render when the state setter is called.
You could create your own custom implementation outside of React, though, one which simulates a re-render by calling a function again when the state setter is called.
const render = () => {
console.log('rendering');
const [value, setValue] = useMyState(0);
document.querySelector('.root').textContent = value;
const button = document.querySelector('.root')
.appendChild(document.createElement('button'));
button.addEventListener('click', () => setValue(value + 1));
button.textContent = 'increment';
};
const useMyState = (() => {
let mounted = false;
let currentState;
return (initialValue) => {
if (!mounted) {
mounted = true;
currentState = initialValue;
}
return [
currentState,
(newState) => {
currentState = newState;
render();
}
];
};
})();
render();
<div class="root"></div>
Every state manager that wants to interact with React has to find a way to connect to React lifecycle, in order to be able to trigger re-renders on state change. useState hook internally uses useReducer:
https://github.com/facebook/react/blob/16.8.6/packages/react-dom/src/server/ReactPartialRendererHooks.js#L254
That's why I made this naive implementation of useState based on JavaScript Proxies and a useReducer dummy dispatch just to force a re-render when state changes.
It's naive, but that's what valtio is based on.
Consider that the power of proxies would make it possible to trigger re-renders by mutating state directly, that's what happens in valtio!
import { useReducer, useCallback, useMemo } from 'react';
export const useMyState = (_state) => {
// FORCE RERENDER
const [, rerender] = useReducer(() => ({}));
const forceUpdate = useCallback(() => rerender({}), []);
// INITIALIZE STATE AS A MEMOIZED PROXY
const { proxy, set } = useMemo(() => {
const target = {
state: _state,
};
// Place a trap on setter, to trigger a component rerender
const handler = {
set(target, prop, value) {
console.log('SETTING', target, prop, value);
target[prop] = value;
forceUpdate();
return true;
},
};
const proxy = new Proxy(target, handler);
const set = (d) => {
const value = typeof d === 'function' ? d(proxy.state) : d;
if (value !== proxy.state) proxy.state = value;
};
return { proxy, set };
}, []);
return [proxy.state, set];
};
Demo https://stackblitz.com/edit/react-33fpbk?file=src%2FApp.js
I am experienced js/React developer but came across case that I can't solve and I don't have idea how to fix it.
I have one context provider with many different state, but one state looks like following:
const defaultParams = {
ordering: 'price_asc',
page: 1,
perPage: 15,
attrs: {},
}
const InnerPageContext = createContext()
export const InnerPageContextProvider = ({ children }) => {
const [params, setParams] = useState({ ...defaultParams })
const clearParams = () => {
setParams({...defaultParams})
}
console.log(defaultParams)
return (
<InnerPageContext.Provider
value={{
params: params,
setParam: setParam,
clearParams:clearParams
}}
>
{children}
</InnerPageContext.Provider>
)
}
I have one button on page, which calls clearParams function and it should reset params to default value.
But it does not works
Even when i console.log(defaultParams) on every provider rerendering, it seems that defaultParams variable is also changing when state changes
I don't think it's normal because I have used {...defaultParams} and it should create new variable and then pass it to useState hook.
I have tried:
const [params, setParams] = useState(Object.assign({}, defaultParams))
const clearParams = () => {
setParams(Object.assign({}, defaultParams))
}
const [params, setParams] = useState(defaultParams)
const clearParams = () => {
setParams(defaultParams)
}
const [params, setParams] = useState(defaultParams)
const clearParams = () => {
setParams({
ordering: 'price_asc',
page: 1,
perPage: 15,
attrs: {},
})
}
None of above method works but 3-rd where I hard-coded same object as defaultParams.
The idea is to save dafult params somewhere and when user clears params restore to it.
Do you guys have some idea hot to make that?
Edit:
This is how I update my params:
const setParam = (key, value, type = null) => {
setParams(old => {
if (type) {
old[type][key] = value
} else old[key] = value
console.log('Params', old)
return { ...old }
})
}
please show how you update the "params".
if there is something like this in the code "params.attrs.test = true" then defaultParams will be changed
if old[type] is not a simple type, it stores a reference to the same object in defaultParams. defaultParams.attrs === params.attrs. Since during initialization you destructuring an object but not its nested objects.
the problem is here: old[type][key] = value
solution:
const setParam = (key, value, type = null) => {
setParams(old => {
if (type) {
old[type] = {
...old[type],
key: value,
}
} else old[key] = value
return { ...old }
})
}
It seems kind of magic to me how React is able to maintain the state variable value inside the functional component between re-renders. So I tried to implement myself to see how things work. But fails. If you can point to some good resources where I can see what goes under the hood to achieve this process or can tell what I am doing wrong in the code. React source is a bit hard to understand at this point for me. Thank you.
const MyReact = {
state: null,
stateInitialized: false,
setState(newState) {
if (this.state !== newState);
{
this.state = newState;
Component();
}
},
useState(initialValue) {
if (!this.stateInitialized) {
this.stateInitialized = true;
this.state = initialValue;
}
return [this.state, this.setState];
},
};
function Component() {
const [count, setCount] = MyReact.useState(10);
console.log(count);
setCount(20);
}
Component();
You have two problems in your code:
Wrong semicolon in setState(): if (this.state !== newState);, which leads to an unconditional call of Component() and thus an endless recursion.
More subtle: In useState(), you return this.setState without binding it to MyReact, which leads to setCount() setting the state globally (i.e. on the window object). Replace it with this.setState.bind(this).
Hooks are triggered by events and in turn, dispatch those event changes to the recorded handler (setCount()).
The Component is a constructor function and must be instantiated (as seen in line 20 of this React Source code). So this source more closely reflects React's structure.
const MyReact = {
state: null,
stateInitialized: false,
setState(newState) {
if (this.state !== newState);
{
this.state = newState;
}
console.log("newCount event calls setState: ",this.state);
},
useState(initialValue) {
this.setState = this.setState.bind(this);
if (!this.stateInitialized) {
this.stateInitialized = true;
this.state = initialValue;
}
console.log("Component calls useState: ",this.state);
return [this.state, this.setState];
},
};
function Component() {
const [count, setCount] = MyReact.useState(10);
this.listener = setCount;
}
const component = new Component();
document.addEventListener("newCount", (e) => { component.listener(e.detail.value) });
// create and dispatch the event
var event = new CustomEvent("newCount", {
detail: {
value: 0
}
});
event.detail.value = 20;
document.dispatchEvent(event);
event.detail.value = 30;
document.dispatchEvent(event);
console.log("MyReact.state = ",MyReact.state);
Problem
useState always triggers an update even when the data's values haven't changed.
Here's a working demo of the problem: demo
Background
I'm using the useState hook to update an object and I'm trying to get it to only update when the values in that object change. Because React uses the Object.is comparison algorithm to determine when it should update; objects with equivalent values still cause the component to re-render because they're different objects.
Ex. This component will always re-render even though the value of the payload stays as { foo: 'bar' }
const UseStateWithNewObject = () => {
const [payload, setPayload] = useState({});
useEffect(
() => {
setInterval(() => {
setPayload({ foo: 'bar' });
}, 500);
},
[setPayload]
);
renderCountNewObject += 1;
return <h3>A new object, even with the same values, will always cause a render: {renderCountNewObject}</h3>;
};
Question
Is there away that I can implement something like shouldComponentUpdate with hooks to tell react to only re-render my component when the data changes?
If I understand well, you are trying to only call setState whenever the new value for the state has changed, thus preventing unnecessary rerenders when it has NOT changed.
If that is the case you can take advantage of the callback form of useState
const [state, setState] = useState({});
setState(prevState => {
// here check for equality and return prevState if the same
// If the same
return prevState; // -> NO RERENDER !
// If different
return {...prevState, ...updatedValues}; // Rerender
});
Here is a custom hook (in TypeScript) that does that for you automatically. It uses isEqual from lodash. But feel free to replace it with whatever equality function you see fit.
import { isEqual } from 'lodash';
import { useState } from 'react';
const useMemoizedState = <T>(initialValue: T): [T, (val: T) => void] => {
const [state, _setState] = useState<T>(initialValue);
const setState = (newState: T) => {
_setState((prev) => {
if (!isEqual(newState, prev)) {
return newState;
} else {
return prev;
}
});
};
return [state, setState];
};
export default useMemoizedState;
Usage:
const [value, setValue] = useMemoizedState({ [...] });
I think we would need to see a better real life example of what you are tying to do, but from what you have shared I think the logic would need to move upstream to a point before the state gets set.
For example, you could manually compare the incoming values in a useEffect before you update state, because this is basically what you are asking if React can do for you.
There is a library use-deep-compare-effect https://github.com/kentcdodds/use-deep-compare-effect that may be of use to you in this case, taking care of a lot of the manual effort involved, but even then, this solution assumes the developer is going to manually decide (based on incoming props, etc) if the state should be updated.
So for example:
const obj = {foo: 'bar'}
const [state, setState] = useState(obj)
useEffect(() => {
// manually deep compare here before updating state
if(obj.foo === state.foo) return
setState(obj)
},[obj])
EDIT: Example using useRef if you don't use the value directly and don't need the component to update based on it:
const obj = {foo: 'bar'}
const [state, setState] = useState(obj)
const { current: payload } = useRef(obj)
useEffect(() => {
// always update the ref with the current value - won't affect renders
payload = obj
// Now manually deep compare here and only update the state if
//needed/you want a re render
if(obj.foo === state.foo) return
setState(obj)
},[obj])
Is there away that I can implement something like shouldComponentUpdate with hooks to tell react to only re-render my component when the data changes?
Commonly, for state change you compare with previous value before rendering with functional useState or a reference using useRef:
// functional useState
useEffect(() => {
setInterval(() => {
const curr = { foo: 'bar' };
setPayload(prev => (isEqual(prev, curr) ? prev : curr));
}, 500);
}, [setPayload]);
// with ref
const prev = useRef();
useEffect(() => {
setInterval(() => {
const curr = { foo: 'bar' };
if (!isEqual(prev.current, curr)) {
setPayload(curr);
}
}, 500);
}, [setPayload]);
useEffect(() => {
prev.current = payload;
}, [payload]);
For completeness, "re-render my component when the data changes?" may be referred to props too, so in this case, you should use React.memo.
If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
The generic solution to this that does not involve adding logic to your effects, is to split your components into:
uncontrolled container with state that renders...
dumb controlled stateless component that has been memoized with React.memo
Your dumb component can be pure (as if it had shouldComponentUpdate implemented and your smart state handling component can be "dumb" and not worry about updating state to the same value.
Example:
Before
export default function Foo() {
const [state, setState] = useState({ foo: "1" })
const handler = useCallback(newValue => setState({ foo: newValue }))
return (
<div>
<SomeWidget onEvent={handler} />
Value: {{ state.foo }}
</div>
)
After
const FooChild = React.memo(({foo, handler}) => {
return (
<div>
<SomeWidget onEvent={handler} />
Value: {{ state.foo }}
</div>
)
})
export default function Foo() {
const [state, setState] = useState({ foo: "1" })
const handler = useCallback(newValue => setState({ foo: newValue }))
return <FooChild handler={handler} foo={state.foo} />
}
This gives you the separation of logic you are looking for.
You can use memoized components, they will re-render only on prop changes.
const comparatorFunc = (prev, next) => {
return prev.foo === next.foo
}
const MemoizedComponent = React.memo(({payload}) => {
return (<div>{JSON.stringify(payload)}</div>)
}, comparatorFunc);
I have a list of warehouses that I pull from an API call. I then render a list of components that render checkboxes for each warehouse. I keep the state of the checkbox in an object (using the useState hook). when I check/uncheck the checkbox, I update the object accordingly.
My task is to display a message above the checkbox when it is unchecked. I tried simply using the object, however, the component was not re-rendering when the object changed.
I found a solution to my problem by simply adding another useState hook (boolean value) that serves as a toggle. Since adding it, the component re-renders and my object's value is read and acted on appropriately.
My question is: why did I have to add the toggle to get React to re-render the component? Am I not updating my object in a manner that allows React to see the change in state? Can someone explain to me what is going on here?
I've created a sandbox to demonstrate the issue: https://codesandbox.io/s/intelligent-bhabha-lk61n
function App() {
const warehouses = [
{
warehouseId: "CHI"
},
{
warehouseId: "DAL"
},
{
warehouseId: "MIA"
}
];
const [warehouseStatus, setWarehouseStatus] = useState({});
const [toggle, setToggle] = useState(true);
useEffect(() => {
if (warehouses.length > 0) {
const warehouseStates = warehouses.reduce((acc, item) => {
acc[item.warehouseId] = true;
return acc;
}, {});
setWarehouseStatus(warehouseStates);
}
}, [warehouses.length]);
const handleChange = obj => {
const newState = warehouseStatus;
const { name, value } = obj;
newState[name] = value;
setWarehouseStatus(newState);
setToggle(!toggle);
};
return warehouses.map((wh, idx) => {
return (
<div key={idx}>
{!warehouseStatus[wh.warehouseId] && <span>This is whack</span>}
<MyCheckbox
initialState
id={wh.warehouseId}
onCheckChanged={handleChange}
label={wh.warehouseId}
/>
</div>
);
});
}
Thanks in advance.
You are mutating state (don't mutate state)
this:
const handleChange = obj => {
const newState = warehouseStatus;
const { name, value } = obj;
newState[name] = value;
setWarehouseStatus(newState);
};
should be:
const handleChange = ({name,value}) => {
setWarehouseStatus({...warehouseStatus,[name]:value});
};
See the problem?
const newState = warehouseStatus; <- this isn't "newState", it's a reference to the existing state
const { name, value } = obj;
newState[name] = value; <- and now you've gone and mutated the existing state
You then call setState with the same state reference (directly mutated). React says, "hey, that's the same reference to the state I previously had, I don't need to do anything".