React: custom hook doesn't work with context - reactjs

I created a custom hook to store Objects in a useState hook and allow changing properties without loosing the other entries.
const useObject = initialValue => {
const [state, setState] = useState(initialValue);
return [
state,
newState => {
setState({
...state,
...newState
});
}
];
};
This hook works in my component but doesn't when I assign it to my context.
Here is what I did:
I created a context:
export const navigation = createContext();
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/store.js:40-83
I created a useObject variable and assigned it as value to my Context Provider
<navigation.Provider value={useObject()}>
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/Layout.js:234-284
I load the context via useContext and change its value
const [navigationState, setNavigationState] = useContext(navigation);
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/App.js:476-616
Result:
The context always stores the new entry and removes all existing entries.
Anyone knows why ?
Here is the Sandbox link. You can test it by clicking the filter button. I expected to see {search:true, icon: 'times'} as context value. Thx!
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/App.js

There is one important things to note here. useEffect in App.js is run once and hence the onClick function set with setNavigationState will use the values from its closure at the point at which it is defined i.e initial render.
Due to this, when you call the function within Header.js from context's the value along with the localState are being reset to the initial value.
SOLUTION 1:
One solution here is to use callback approach to state update. For that you need to modify your implementation on useObject a bit to provide the use the capability to use the callback value from setState
const useObject = initialValue => {
const [state, setState] = useState(initialValue);
return [
state,
newState => {
if(typeof newState === 'function') {
setState((prev) => ({ ...prev, ...newState(prev)}));
} else {
setState({
...state,
...newState
});
}
}
];
};
and then use it in onContextClick function like
const onContextClick = () => {
setState(prevState => {
setNavigationState(prev => ({ icon: ICON[prevState.isOpen ? 0 : 1] }));
return { isOpen: !prevState.isOpen };
});
};
Working DEMO
SOLUTION 2:
The other simpler approach to solving the problem is to use useCallback for onContextClick and update the navigation state with useEffect, everytime the closure state is updated like
const onContextClick = React.useCallback(() => {
setNavigationState({ icon: ICON[state.isOpen ? 0 : 1] });
setState({ isOpen: !state.isOpen });
}, [state]);
useEffect(() => {
setNavigationState({
search: true,
icon: ICON[0],
onClick: onContextClick
});
}, [onContextClick]);
Working demo

Related

Invalid custom hook call

I'm just a react beginner. I'm trying to create a custom hook, which will be triggered once an onClick event is triggered. By what I see, I need to use the useRef hook, to take into account if the component is rendered by first time, or if it's being re-rendered.
My code approach is the next:
const Clear = (value) => {
const useClearHook = () => {
const stateRef = useRef(value.value.state);
console.log(stateRef);
useEffect(() => {
console.log("useEffect: ");
stateRef.current = value.value.state;
stateRef.current.result = [""];
stateRef.current.secondNumber = [""];
stateRef.current.mathOp = "";
console.log(stateRef.current);
value.value.setState({
...stateRef.current,
result: value.value.state.result,
secondNumber: value.value.state.secondNumber,
mathOp: value.value.state.mathOp,
});
}, [stateRef.current]);
console.log(value.value.state);
};
return <button onClick={useClearHook}>Clear</button>;
};
Any suggestion? Maybe I might not call ...stateRef.current in setState. I'm not sure about my mistake.
Any help will be appreciated.
Thanks!
Your problem is useClearHook is not a component (the component always goes with the first capitalized letter like UseClearHook), so that's why when you call useRef in a non-component, it will throw that error. Similarly, for useEffect, you need to put it under a proper component.
The way you're using state is also not correct, you need to call useState instead
Here is a possible fix for you
const Clear = (value) => {
const [clearState, setClearState] = useState()
const useClearHook = () => {
setClearState((prevState) => ({
...prevState,
result: [""],
secondNumber: [""],
mathOp: "",
}));
};
return <button onClick={useClearHook}>Clear</button>;
};
If your states on the upper component (outside of Clear). You can try this way too
const Clear = ({value, setValue}) => {
const useClearHook = () => {
setValue((prevState) => ({
...prevState,
result: [""],
secondNumber: [""],
mathOp: "",
}));
};
return <button onClick={useClearHook}>Clear</button>;
};
Here is how we pass it
<Clear value={value} setValue={setValue} />
The declaration for setValue and value can be like this in the upper component
const [value, setValue] = useState()

How to properly compose functional components that have infinite scrolling logic

I'm converting a class component to functional component for practice. It has a ref object to contain some variables for the component, such as IntersectionObserver object to implement infinite scrolling.
The issue starts from here. The callback function of the IntersectionObserver calls a function(says update) defined in the component to load more data. Because the IntersectionObserver is defined inside the useRef, the update function is the function bound when the component gets initialized. So the value of the state that is used in the update function is also the value of the initial state.
How can I compose this functional component in a proper way?
Backbone demo
export default function A(props) {
const [state, setState] = useState({
pageNo: 1,
isLoading: false,
items: []
});
const update = useCallback(() => {
setState(state => ({...state, isLoading: true}));
someApi(state.pageNo);
setState(state => ({
...state,
pageNo: pageNo + 1
}));
setState(state => ({...state, isLoading: false}));
}, [isLoading, pageNo]);
const observerCallback = useCallback((entries, observer) => {
for (const entry of entries) {
if (entry.isIntersecting) {
observer.disconnect();
update();
}
}
}, [update]);
const observer = useRef(new IntersectionObserver(observerCallback)); // The callback is the function binding the update function that binds some of the initial state
const lastEl = useRef(null);
const preLastEl = useRef(null);
useEffect(() => {
update();
}, [props]);
if (lastEl.current && lastEl.current != preLastEl.current) {
preLastEl.current = lastEl.current;
observer.observe(lastEl.current);
}
return (
<SomeProgressBar style={{ display: state.isLoading ? "" : "none" }}/>
{
state.items.map((item) => <B ... ref={lastEl}/>)
}
);
}
I don't exactly why you're using the ref and why you can't do it differently. so in case you have to do it this way, your refs are dependent to state object and they need to be changed when the state are changed so you should use a useEffect to change the refs based on new state. try to implement one of these two steps:
1
const refs = useRef({
lastEl: undefined,
observer: new IntersectionObserver((entries, observer) => {
...
update(state.pageNo); // This is the update function bound when the instance of this component gets initialized
});
});
useEffect(() => {
update(state.pageNo);
}, [props]);
function update(pageNo = 1) {
setState(prev => ({...prev, isLoading: true}));
someApi(pageNo); // state.pageNo will be always 1
setState(prev => ({...prev, isLoading: false}));
}
2 in case above code didn't work try this
useEffect(() => {
if(state.pageNo){
refs.current = {
lastEl: undefined,
observer: new IntersectionObserver((entries, observer) => {
...
update(state.pageNo); // This is the update function bound when the instance of this component gets initialized
});
}
}
}, [state.pageNo])

Custom hook reset to its initial state

Hook
export const useCreateAccount = () => {
const [state, setState] = useState(initialState)
const onChangeInput: ChangeEventFunction = useCallback(({ target }) => {
if (!target.files) {
return setState({ ...state, [target.name]: target.value })
}
setState({ ...state, [target.name]: target.files[0] })
}, [])
return { onChangeInput }
}
Component
const { onChangeInput } = useCreateAccount()
<form>
<input name="name1" onChange={onChangeInput}>
<input name="name2" onChange={onChangeInput}>
</form>
Every time I do some change in second input(name2) the previous state(name1) of the component has been lost(reset to initial state), The reason I use 'useCallback', I only need one instance of 'onChangeInput'
But if I remove 'useCallback', state is keeping the previous values(name1)
I can't understand this behavior in hooks, can someone elaborate more on this?
From the docs:
Any function inside a component, including event handlers and effects, “sees” the props and state from the render it was created in.
Here, when you are using useCallback, the function has been defined in it's initial render and has the initial state defined then. This is the reason why useCallback has a depedency array that can be used to refresh the function and values used inside it.
But you cannot use state as a dependency because you are setting the same inside it, instead you can use the functional version of setState so as to get the previous values of state instead of reffering to the central one.
const onChangeInput: ChangeEventFunction = useCallback(({ target }) => {
if (!target.files) {
return setState(prevState => ({ ...prevState, [target.name]: target.value }));
}
setState(prevState => ({ ...prevState, [target.name]: target.files[0] }))
}, [])

React Hooks stale state inside function

I have something like this
const [state, setState] = useState({
num: 0
});
const validateBiggerThan0 = () => {
return state.num > 0;
};
const [validation, setValidation] = useState({
val: validateBiggerThan0
});
The problem is when I call validation.validateBiggerThan0, this function does not have the current state.
In this simple example, I could pass the value sate.num as a parameter to the validateBiggerThan0, but in my real application this is not feasible because I have multiple validations and some can use more than one value from the state. Is there any alternative to this?
A sandbox with the problem:
https://codesandbox.io/s/heuristic-mountain-2poz7?fontsize=14&hidenavigation=1&theme=dark
The solution to your problem is to not use useState for the function object.
const validation = {
val: validateBiggerThan0
};
Or to update the object on every change of your state:
useEffect(() => {
setValidation({
val: validateBiggerThan0
});
}, [ state, setValidation, validateBiggerThan0 ]);
// Don't remove "state" from the dependency array.
Don't forget to wrap your validation functions in a useCallback hook in case you use the useEffect method.
const validateBiggerThan0 = useCallback(() => {
return state.num > 0;
}, [state]);

Function not correctly reading updated state from React hook state

I was trying to implement a simple paint component with React hooks. My expected behavior was 'mouseMove' to be executed when I moved my mouse while remaining clicked. However, state.isMouseDown always returned false within mouseMove().
Any fixes or references to potentially helpful documents would be grateful.
const initialState = {
isMouseDown: false,
isMouseMoving: false
};
const DrawingCanvas = () => {
const [state, setState] = useState(initialState);
useEffect(() => {
console.log('mounted');
document.addEventListener('mousedown', () => mouseDown());
document.addEventListener('mousemove', () => mouseMove());
}, []);
const mouseDown = () => {
console.log('mousedown');
setState(state => ({
...state,
isMouseDown: true
}));
};
const mouseMove = () => {
// why is this false even when click and move?
console.log('mouseMove:isMouseDown', state.isMouseDown);
if (!state.isMouseDown) return;
console.log('mousemove'); // this line is not being executed
setState(state => ({
...state,
isMouseMoving: true
}));
};
console.log(state);
return (
<div>
<p>mouseDown: {`${state.isMouseDown}`}</p>
<p>mouseMoving: {`${state.isMouseMoving}`}</p>
</div>
);
};
As explained in this related answer, the problem is that event listener accesses state object from the scope where it was defined, i.e. initial state, because event is listened on component mount.
A solution is to either use mutable state, or access state exclusively from state updater function. In the code above, state.isMouseDown refers to original state. In case it's needed to avoid state updates, state updater can return original state:
const mouseMove = () => {
setState(state => {
if (!state.isMouseDown)
return state; // skip state update
else
return {
...state,
isMouseMoving: true
};
});
};

Resources