How to setState with hooks in EventEmitter's on('change') function? - javascript

For the purpose of learning I am trying to breakdown the basics of the flux pattern by implementing it from scratch. I know that I can fix the problem below using a Class Component but why does it work with the class component's setState({ ... }) and not with the hook's setValues function?
As you can guess, the problem is that the setValues function does not seem to trigger a re-render with the new state. I have a feeling this has to do with closures, but I don't understand them super well, so I would appreciate any guidance and help.
Store
// store.js
import EventEmitter from "events";
class StoreClass extends EventEmitter {
constructor() {
super();
this.values = []
}
addNow() {
this.values.push(Date.now());
this.emit('change')
}
getAll() {
return this.values;
}
}
const store = new StoreClass();
export default store;
Component
// App.js
import store from './store';
function App() {
const [values, setValues] = useState([]);
const handleClick = () => {
store.addNow();
};
useEffect(() => {
const onChangeHandler = () => {
console.log("Change called");
setValues(() => store.getAll())
};
store.on('change', onChangeHandler);
return () => store.removeAllListeners('change')
}, [values]);
return (
<div>
<button onClick={handleClick}>Click to add</button>
<ul>
{values.map(val => <li key={val}>{val}</li>)}
</ul>
</div>
);
}

the useEffct listen to changes in values but setValues only exist in the useEffect, I don't think you need the useEffect at all
you can try something like this:
// App.js
import store from './store';
function App() {
const [values, setValues] = useState([]);
const onChangeHandler = () => {
console.log("Change called");
const storeValues = store.getAll();
setValues(storeValues);
};
const handleClick = () => {
store.addNow();
onChangeHandler();
};
return (
<div>
<button onClick={handleClick}>Click to add</button>
<ul>
{values.map(val => <li key={val}>{val}</li>)}
</ul>
</div>
);
}

After tinkering a few hours more and reading the react docs again, I can finally answer my own question. In short the problem is that the state in the store was not updated immutably.
Redux, which is an implementation of the Flux architecture, makes it very clear in their documentation:
Redux expects that all state updates are done immutably
Therefore, the culprit in the above code is in the addNow function of the store. The Array.push() method mutates the existing array by appending an item. The immutable way of doing this would be to either use the Array.concat() method or the ES6 spread syntax (de-structuring) [...this.values] and then assigning the new array to the this.values.
addNow() {
this.values.push(Date.now()); // Problem - Mutable Change
this.values = this.values.concat(Date.now()) // Option 1 - Immutable Change
this.values = [...this.values, Date.now()] // Option 2 - Immutable Change
this.emit('change')
}
To see the effect of each of these changes in the react component, change the setValues function in the useEffect hook to the following:
setValues((prev) => {
const newValue = store.getAll()
console.log("Previous:", prev)
console.log("New value:", newValue)
return newValue
})
In the console it will become clear that when the state in the store is changed mutably, after the first button click, the previous state and the new state will always be the same and reference the same array. However when the state in the store is changed immutably, then the previous state and new state do not reference the same array. Besides, in console it will be clear that the previous state array has one less value than the new state array.
Why does the state change trigger a re-render when using the class component but not when using the hooks?
According to the react docs for the class component's setState function.
setState() will always lead to a re-render unless shouldComponentUpdate() returns false.
This means that whether the state in the store is changed mutably or immutably, when the change event is fired and the setState function is called, react will re-render the component and display the "new" state. This however is not the case with the useState hook.
According to the react docs for the useState hook:
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
This means that when the the change to the array in the store is done by mutating the original object, react checks if the new state and the previous are the same before deciding to render again. Since the reference for both the previous and current state are the same, react does nothing. To fix this, the change in the store has to be done immutably.

Related

useState setter function is not updating state in react

I am using the useIsDirty hook in two components, CustomCodeEditor and EditorFooter, to track whether the code in the Editor has been modified. The hook returns an isDirty state and a setIsDirty function to update it. When I call setIsDirty(true) in the CustomCodeEditor component, the state is updated, but when I call setIsDirty(false) in the EditorFooter component, it doesn't seem to update the isDirty state. I believe this is because the EditorFooter component does not have access to the updated state. Anyone, please help me with this.
useIsDirty:
import { useEffect, useState } from "react"
const useIsDirty = () => {
const [isDirty, setIsDirty] = useState(false)
useEffect(() => {
const handleBeforeUnload = (event) => {
if (isDirty) {
event.preventDefault()
event.returnValue = ""
alert("You have unsaved changes, are you sure you want to leave?")
}
}
console.log("Diryt:", isDirty)
window.addEventListener("beforeunload", handleBeforeUnload)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [isDirty])
return { isDirty, setIsDirty }
}
export default useIsDirty
CustomCodeEditor
import Editor from "#monaco-editor/react"
import useIsDirty from "../../hooks/useIsDirty"
const CustomCodeEditor = () => {
const { isDirty, setIsDirty } = useIsDirty()
console.log("isDirty:", isDirty)
return (
<div className="bg-[#1e1e1e] h-full">
<Editor
onChange={(value) => {
updateCode(value || "")
setIsDirty(true) // updating state
}}
/>
</div>
)
}
export default CustomCodeEditor
EditorFooter
import Button from "../reusable/Button"
const EditorFooter = () => {
const { setIsDirty } = useIsDirty()
const handleSave = async () => {
setIsDirty(false)
}
return (
<div>
<Button
onClick={handleSave}
>
Save
</Button>
<Button
onClick={handleSave}
>
Submit
</Button>
</div>
)
}
export default EditorFooter
Hooks are not singleton instances.. when you use useIsDirty somewhere.. it always create new instance, with unrelated states to other ones. If you want to share this state you need to use Context
const IsDirtyContext = createContext(undefined);
const IsDirtyProvider = ({ children }): ReactElement => {
const [isDirty, setIsDirty] = useState(false)
return <IsDirtyContext.Provider value={{isDirty, setIsDirty}}>{children}</IsDirtyContext.Provider>;
};
and then you should wrap your commponent tree where you wanna access it with IsDirtyProvider
after that, you can even create your custom hook that will just return that context:
const useIsDirty = () => {
return useContext(IsDirtyContext)
}
Looking at your question, it looks like you are trying to use the same state in both components. However, the state doesn't work like that. A new instance is created whenever you make a call to useIsDirty from a different component.
If you want to use the state value across two components. You can do that using one of the following ways.
1 - Use a parent and child hierarchy.
Steps
Create a parent component and wrap the two components inside the parent component.
Manage the state in the parent component and pass it using props to the child component.
Create a function in child components that will execute a function from the parent component. The parent component function will hold the code to update the state based on whatever value you receive from the child component.
Now you should be able to share your state between both components.
2 - Use the context api.
If you are not familiar with what context api is, below is a brief explanation.
Context api helps you share data between components, without the need of passing them as a prop to each and every component.
You can use createContext and useContext hooks from context api.
createContext is used to create a new context provider.
useContext hook is used to manage the state globally.
You can get the value from context using this function.
Whenever the state is updated the value will be reflected globally.
Note - Any component that needs to use the value inside the useContext should be wrapped inside the useContext provider component.
Steps to create a context provider.
To create a context you just need to use the react hook createContext
Create a context using below code
const isDirtyContext = createContext();
Wrap your components in the context provider
import {IsDirtyContext} from './path/filename'
<IsDirtyContext.Provider value={[isDirty, setIsDirty]}>{children}</IsDirtyContext.Provider>
If your context is in a separate file, then you can import it into any child component using the import statement.
import {IsDirtyContext} from './path/filename'
Use the context
const [isDirty] = useContext(IsDirtyContext);
Now the isDirty state value is available globally in all components.
Hope this information helps you. Please upvote if this helps you understand and solve the problem.

Invalid hook call error when trying to set state

I have a scenario where I am forced to call a trigger method to show a modal from two different places, one using a hotkey combination and another by clicking on a toolbar button. In order to do so I have the following code, where I call the triggerCustomLinkModal to set the state but then I am hit with the Invalid Hook call error.
import { useState, useCallback, useEffect } from "react"
import { Dialog } from "#blueprintjs/core"
const useLocalState = () => {
const [isShown, setIsShown] = useState(false)
const setState = useCallback((state) => {
setIsShown(state)
})
const getState = useCallback(() => {
return isShown
})
return {
setState,
getState
}
}
export const CustomLinkModalUI = () => {
const { getState } = useLocalState()
return (
<>
<Dialog isOpen={getState()} />
</>
)
}
export const triggerCustomLinkModal = () => {
const { setState } = useLocalState()
setState()
}
Expanding from Chris answer in the comments ( You can't use hooks outside React components. -> so you can't call useLocalState() inside triggerCustomLinkModal since triggerCustomLinkModal is not a React component ):
You don't really need the useCallback hook or even the functions itself. Aaccording to react docs :
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
This also means that using useCallback hook to set a state it doesn't really make sense (because useCallback role is just to return a memoized callback)
What you basically need is a state set up in the closest parrent component and pass the setIsShown as a prop as well as the isShown function.
Your current implementation, even if it weren't for the error, it wouldn't refer to the same state since on each useLocalState() you are initializing a fresh new state (so you are not pointing to the same state in CustomLinkModalUI and triggerCustomLinkModal)

Using a global object in React Context that is not related to state

I want to have a global object that is available to my app where I can retrieve the value anywhere and also set a new value anywhere. Currently I have only used Context for values that are related to state i.e something needs to render again when the value changes. For example:
import React from 'react';
const TokenContext = React.createContext({
token: null,
setToken: () => {}
});
export default TokenContext;
import React, { useState } from 'react';
import './App.css';
import Title from './Title';
import TokenContext from './TokenContext';
function App() {
const [token, setToken] = useState(null);
return(
<TokenContext.Provider value={{ token, setToken }}>
<Title />
</TokenContext.Provider>
);
}
export default App;
How would I approach this if I just want to store a JS object in context (not a state) and also change the value anywhere?
The global context concept in React world was born to resolve problem with passing down props via multiple component layer. And when working with React, we want to re-render whenever "data source" changes. One way data binding in React makes this flow easier to code, debug and maintain as well.
So what is your specific purpose of store a global object and for nothing happen when that object got changes? If nothing re-render whenever it changes, so what is the main use of it?
Prevent re-render in React has multiple ways like useEffect or old shouldComponentUpdate method. I think they can help if your main idea is just prevent re-render in some very specific cases.
Use it as state management libraries like Redux.
You have a global object (store) and you query the value through context, but you also need to add forceUpdate() because mutating the object won't trigger a render as its not part of React API:
const globalObject = { counter: 0 };
const Context = React.createContext(globalObject);
const Consumer = () => {
const [, render] = useReducer(p => !p, false);
const store = useContext(Context);
const onClick = () => {
store.counter = store.counter + 1;
render();
};
return (
<>
<button onClick={onClick}>Render</button>
<div>{globalObject.counter}</div>
</>
);
};
const App = () => {
return (
<Context.Provider value={globalObject}>
<Consumer />
</Context.Provider>
);
};

how to create React Context for a function value without re-rendering all the time

I am using React with hooks (no class components).
I currently have code like this:
function useTimerange()
{
const [tr, setTr] = useState(defaultRange);
function setCustomRange(range)
{
setTr(range);
}
return {tr, setCustomRange};
}
const ContextSetCustomTimerange = React.createContext(undefined);
function MyComponent(props)
{
const {tr, setCustomRange} = useTimerange();
return (
<>
<TimeRangePicker selectedRange={tr}/>
<ContextSetCustomTimerange.Provider value={setCustomRange}>
<MyChildComponent/>
</ContextSetCustomTimerange.Provider>
</>
);
}
The problem is on this line:
<ContextSetCustomTimerange.Provider value={setCustomRange}>
since the value being passed is a function that is recreated each time MyComponent renders, it will cause the Context provider to re-render all its consumers every time.
What is the best way to prevent this?
You can use useCallback from react hooks. https://reactjs.org/docs/hooks-reference.html#usecallback
import { useCallback } from 'react'
function useTimerange()
{
const [tr, setTr] = useState(defaultRange);
const setCustomRange = useCallback((range) => {
setTr(range);
}, [])
return {tr, setCustomRange};
}
or may be just return direct setTr
return {tr, setCustomRange: setTr};
The performance issue here is that if setCustomRange changes it's reference it will trigger a render an all consumers for your context, since react is comparing references.
I think useCallback with [], like Giang mentions, will do it at it seems like the best idea.
You can also pass a state to the provider value and put the function on the state; since the state reference is kept and when react compares the old value with the new value it will detect the're the same and leave the thing alone.

How to listen state changes in react.js?

What is the angular's $watch function equivalent in React.js?
I want to listen state changes and call a function like getSearchResults().
componentDidMount: function() {
this.getSearchResults();
}
The following lifecycle methods will be called when state changes. You can use the provided arguments and the current state to determine if something meaningful changed.
componentWillUpdate(object nextProps, object nextState)
componentDidUpdate(object prevProps, object prevState)
In 2020 you can listen to state changes with the useEffect hook like this
export function MyComponent(props) {
const [myState, setMystate] = useState('initialState')
useEffect(() => {
console.log(myState, '- Has changed')
},[myState]) // <-- here put the parameter to listen
}
I haven't used Angular, but reading the link above, it seems that you're trying to code for something that you don't need to handle. You make changes to state in your React component hierarchy (via this.setState()) and React will cause your component to be re-rendered (effectively 'listening' for changes).
If you want to 'listen' from another component in your hierarchy then you have two options:
Pass handlers down (via props) from a common parent and have them update the parent's state, causing the hierarchy below the parent to be re-rendered.
Alternatively, to avoid an explosion of handlers cascading down the hierarchy, you should look at the flux pattern, which moves your state into data stores and allows components to watch them for changes. The Fluxxor plugin is very useful for managing this.
Since React 16.8 in 2019 with useState and useEffect Hooks, following are now equivalent (in simple cases):
AngularJS:
$scope.name = 'misko'
$scope.$watch('name', getSearchResults)
<input ng-model="name" />
React:
const [name, setName] = useState('misko')
useEffect(getSearchResults, [name])
<input value={name} onChange={e => setName(e.target.value)} />
I think you should be using below Component Lifecycle as if you have an input property which on update needs to trigger your component update then this is the best place to do it as its will be called before render you even can do update component state to be reflected on the view.
componentWillReceiveProps: function(nextProps) {
this.setState({
likesIncreasing: nextProps.likeCount > this.props.likeCount
});
}
If you use hooks like const [ name , setName ] = useState (' '), you can try the following:
useEffect(() => {
console.log('Listening: ', name);
}, [name]);
Using useState with useEffect as described above is absolutely correct way. But if getSearchResults function returns subscription then useEffect should return a function which will be responsible for unsubscribing the subscription . Returned function from useEffect will run before each change to dependency(name in above case) and on component destroy
It's been a while but for future reference: the method shouldComponentUpdate() can be used.
An update can be caused by changes to props or state. These methods
are called in the following order when a component is being
re-rendered:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
ref: https://reactjs.org/docs/react-component.html
I use this code to see which one in the dependencies changes. This is better than the pure useEffect in many cases.
// useWatch.js
import { useEffect, useMemo, useRef } from 'react';
export function useWatchStateChange(callback, dependencies) {
const initialRefVal = useMemo(() => dependencies.map(() => null), []);
const refs = useRef(initialRefVal);
useEffect(() => {
for(let [index, dep] of dependencies.entries()) {
dep = typeof(dep) === 'object' ? JSON.stringify(dep) : dep;
const ref = refs.current[index];
if(ref !== dep) {
callback(index, ref, dep);
refs.current[index] = dep;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies);
}
And in my React component
// App.js
import { useWatchStateChange } from 'useWatch';
...
useWatchStateChange((depIndex, prevVal, currentVal) => {
if(depIndex !== 1) { return } // only focus on dep2 changes
doSomething("dep2 now changes", dep1+dep2+dep3);
}, [ dep1, dep2, dep3 ]);

Categories

Resources