Given this simple custom hook
import React, { createContext, useContext } from 'react';
const context = {
__prefs: JSON.parse(localStorage.getItem('localPreferences') || null) || {} ,
get(key, defaultValue = null) {
return this.__prefs[key] || defaultValue;
},
set(key, value) {
this.__prefs[key] = value;
localStorage.setItem('localPreferences', JSON.stringify(this.__prefs))
}
};
const LocalPreferenceContext = createContext(context);
export const useLocalPreferences = () => useContext(LocalPreferenceContext);
export const withLocalPreferences = Component => () => <Component localPreferences={ useLocalPreferences() } />;
When I use either of these, calling set on the context does not update anything. Sure, how React would know that I have updated anything? But what could be done to make it work (excluding using a Provider)?
** Edit **
Ok, so what is the alternative other than using useContext then? That's the real question, really; how do I update the components using this hook (or HOC)? Is useState the only way? How? Using some event emitter?
I think using context does make sense here, but you will need to use a provider, as that's a core part of how context works. Rendering a provider makes a value available to components farther down the tree, and rendering with a new value is what prompts the consumers to rerender. If there's no provider than you can at least get access to a default value (which is what you have in your code), but the default never changes, so react has nothing to notify the consumers about.
So my recommendation would be to add in a component with a provider that manages the interactions with local storage. Something like:
const LocalPreferenceProvider = () => {
const [prefs, setPrefs] = useState(
() => JSON.parse(localStorage.getItem("localPreferences") || null) || {}
);
// Memoized so that it we don't create a new object every time that
// LocalPreferenceProvider renders, which would cause consumers to
// rerender too.
const providedValue = useMemo(() => {
return {
get(key, defaultValue = null) {
return prefs[key] || defaultValue;
},
set(key, value) {
setPrefs((prev) => {
const newPrefs = {
...prev,
[key]: value,
};
localStorage.setItem("localPreferences", JSON.stringify(newPrefs));
return newPrefs;
});
},
};
}, [prefs]);
return (
<LocalPreferenceContext.Provider value={providedValue}>
{children}
</LocalPreferenceContext.Provider>
);
};
You mentioned in the comments that you wanted to avoid having a bunch of nested components, and you already have a big stack of providers. That is something that will often happen as the app grows in size. Personally, my solution to this is to just extract the providers into their own component, then use that component in my main component (something like<AllTheProviders>{children}</AllTheProviders>). Admittedly this is just an "out of sight, out of mind" solution, but that's all i really tend to care about for this case.
If you do want to completely get away from using providers, then you'll need to get away from using context too. It may be possible to set up a global object which is also an event emitter, and then have any components that want to get access to that object subscribe to the events.
The following code is incomplete, but maybe something like this:
const subscribers = [];
let value = 'default';
const globalObject = {
subscribe: (listener) => {
// add the listener to an array
subscribers.push(listener);
// TODO: return an unsubscribe function which removes them from the array
},
set: (newValue) {
value = newValue;
this.subscribers.forEach(subscriber => {
subscriber(value);
});
},
get: () => value
}
export const useLocalPreferences = () => {
let [value, setValue] = useState(globalObject.get);
useEffect(() => {
const unsubscribe = globalObject.subscribe(setValue);
return unsubscribe;
}, []);
return [value, globalObject.set];
})
You could pull in a pub/sub library if you don't want to implement it yourself, or if this is turning into to much of a project, you could use an existing global state management library like Redux or MobX
Related
I would like to figure out how to subscribe on updates of a stored value it the redux store.
So far I've tried something like the following:
<ReactReduxContext.Consumer>
{({store}) => {
console.log('store:', store.getState());
const p = <p>{store.getState().value}</p>;
store.subscribe(() => {p.innerText = store.getState().value});
return p;
}}
</ReactReduxContext.Consumer>
bumping into the TypeError: can't define property "innerText": Object is not extensible error on updates.
So I wonder how to update the contents?
There are a few things about your code that are just not the way that we do things in React.
React is its own system for interacting with the DOM, so you should not attempt direct DOM manipulation through .innerText. Your code doesn't work because the variable p which you create is a React JSX Element rather than a raw HTML paragraph element, so it doesn't have properties like innerText.
Instead, you just return the correct JSX code based on props and state. The code will get updated any time that props or state change.
The ReactReduxContext is used internally by the react-redux package. Unless you have a good reason to use it in your app, I would not recommend it. There are two built-in ways that you can get a current value of state that is already subscribed to changes.
useSelector hook
(recommended)
export const MyComponent1 = () => {
const value = useSelector(state => state.value);
return <p>{value}</p>
}
connect higher-order component
(needed for class components which cannot use hooks)
class ClassComponent extends React.Component {
render() {
return <p>{this.props.value}</p>
}
}
const mapStateToProps = state => ({
value: state.value
});
const MyComponent2 = connect(mapStateToProps)(ClassComponent)
ReactReduxContext
(not recommended)
If anyone reading this has a good reason why they should need to use store.subscribe(), proper usage would look something like this:
const MyComponent3 = () => {
const { store } = useContext(ReactReduxContext);
const [state, setState] = useState(store.getState());
useEffect(() => {
let isMounted = true;
store.subscribe(() => {
if (isMounted) {
setState(store.getState());
}
});
// cleanup function to prevent calls to setState on an unmounted component
return () => {
isMounted = false;
};
}, [store]);
return <p>{state.value}</p>;
};
CodeSandbox Demo
I have an object that self-updates. Let's say every second it changes its state.
export class Obj {
val = 0;
start = () => {
setInterval(() => {
this.update();
}, 1000);
}
update = () => {
this.val++;
}
}
I think (please correct me if I'm wrong) that a similar alternative would be an object connected to a stream of data, changing its state every few ms to the last value received.
I now want a react component (using hooks) that displays this:
function C(obj) {
return <div>{obj.val}</div>
}
My question is, how can I make the component C change when the val of obj changes? I could use a callback on every change, but it feels like I'd be polluting my models only for the sake of React. An alternative would be to move the interval outside of Obj into the React component but that might not be what I want, and would not cover the case of a stream.
Is there a standard approach in React to update a (functiona, hook based) component when some of its props changes its state?
To update React component you need to use any React API, render component's parent (if not memoized), or change its props. Updating the value of an object/class won't notify React.
There are many approaches to make React aware of the object change like the one you described.
A less familiar one is using this.forceUpdate it manually forces a render of a component, it widely uses in React wrappers for common libraries.
Here is a usage example with hooks:
class Obj {
val = 0;
start = sideEffect => {
setInterval(() => {
this.update();
sideEffect();
}, 1000);
};
update = () => {
this.val++;
console.log(this.val);
};
}
const obj = new Obj();
const App = () => {
const [, render] = useReducer(p => !p, false);
useEffect(() => {
const renderOnUpdate = () => {
obj.start(render);
};
renderOnUpdate();
}, []);
return <>{obj.val}</>;
};
The component needs to have a state, which can be updated via the setState method. When you have a changing prop, it might make sense to provide a callback method from your react component, which, when called, executes setState under the hood.
In this case component C won't be re-rendered since its props (obj) has never been changed. Objects are compared by reference and since it is the same object reference that was before nothing happens.
You can use component C like this to make it re-render: <C value={obj.val} />, so on every obj.val update it will react correspondingly since its prop now is changing.
Also you could have this logic inside:
const obj = new Obj();
function C() {
const [value, setValue] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(() => {
setValue(value + 1);
}, 1000);
return () => {
clearInterval(interval); // don't forget to do cleanup
};
}, []);
return <div>{value}</div>
}
I recently game across the following article State Management with React Hooks — No Redux or Context API. Since reacts inception the most talked about issue is always state management and global state. Redux has been the popular choice and more recently the context API. But this approach seems to be much easier, less code and more scalable.
My question is can anyone see a down side to using the this type of state management approach that I may have overlooked. I have tweeked the code a little to support SSR and it works in Nextjs and also made it a little more friendly to use actions and the setting of the state variable.
useGlobalState.js
import React, { useState, useEffect, useLayoutEffect } from 'react';
const effect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
function setState(newState) {
if (newState === this.state) return;
this.state = newState;
this.listeners.forEach((listener) => {
listener(this.state);
});
}
function useCustom() {
const newListener = useState()[1];
effect(() => {
this.listeners.push(newListener);
return () => {
this.listeners = this.listeners.filter((listener) => listener !== newListener);
};
}, []);
return [this.state, this.setState, this.actions];
}
function associateActions(store, actions) {
const associatedActions = {};
if (actions) {
Object.keys(actions).forEach((key) => {
if (typeof actions[key] === 'function') {
associatedActions[key] = actions[key].bind(null, store);
}
if (typeof actions[key] === 'object') {
associatedActions[key] = associateActions(store, actions[key]);
}
});
}
return associatedActions;
}
const useGlobalHook = (initialState, actions) => {
const store = { state: initialState, listeners: [] };
store.setState = setState.bind(store);
store.actions = associateActions(store, actions);
return useCustom.bind(store, React);
};
export default useGlobalHook;
Then set up a custom hook for a state variable can be a simple string or a object here is a simple one:
import useGlobalState from './useGlobalState';
const initialState = 'Hi';
// Example action for complex processes setState will be passed to component for use as well
const someAction = (store, val) => store.setState(val);
const useValue = useGlobalState(initialState, { someAction });
export default useValue;
And use in component:
import React from 'react'
import useVal from './useVal'
export default () => {
const [val, setVal, actions] = useVal();
const handleClick = () => {
setVal('New Val');
// or use some actions
actions.someAction('New Val');
}
return(
<div>{val}</div>
<button onClick={handleClick}>Click Me</button>
)
}
This all seems like a much cleaner and easier approach and I am wondering why this isn't the go to approach for state management in react. First you don't have to wrap everything in a provider. Next it is extremely easy to implement and much less code is involved in the actual app. Can anyone see a downside to using this approach. The only thing I can think of is the re rendering issue that the context api has but in small chunks this shouldn't be an issue.
I have been using a similar approach and I really like it. I actually can't believe more people don't talk about this approach. I wrote a custom hook here React Global Store Hook. It gives you the freedom to dispatch from anywhere in the app and shallow compares to avoid unwanted re-renders. I don't see any performance issues as long as you can avoid the unwanted re-renders.
In all it is a simple concept. You basically create a function to store your state and return 2 functions. One will be a function to set the stored state and one will be a hook to be used in the react component. In the hook you grab the setState function of react on initial render with a createEffect and store it in an array. You can then use this setState function to re render your component. So when you call the dispatch function you can just loop through these setState functions and call them.
Simple example:
import { useState, useEffect } from 'react'
const createStore = (initialStore) => {
let store = initialStore
const listeners = new Set()
const dispatch = (newStore) => {
// Make it like reacts setState so if you pass in a function you can get the store value first
store = typeof newStore === 'function' ? newStore(store) : newStore
listeners.forEach(listener => listener(() => store))
}
const useStore = () => {
const [, listener] = useState()
useEffect(() => {
listeners.add(listener)
return () => listeners.delete(listener)
}, [])
return store
}
return [useStore, dispatch]
}
Then just create a store and use in your component
const [useStore, dispatch] = createStore(0)
const Display = () => {
const count = useStore()
return <div>{count}</div>
}
const addToCount = () =>
<button onClick={ () => dispatch(count => count + 1}>+</button>
Then if you want to avoid re renders you can do a shallow compare in the dispatch function to compare the store to the new store similar to what redux does. Something like the following:
const shouldUpdate = (a, b) => {
for( let key in a ) {
if(a[key] !== b[key]) return true
}
return false
}
and then in dispatch you can check this before firing the listener in your forEach loop.
const dispatch = (newStore) => {
if(!shouldUpdate(
store,
store = typeof newStore === 'function' ? newStore(store) : newstore
) return
listeners.forEach(listener => listener(() => store))
}
Its way less boilerplate than redux and seems to be much cleaner. The best thing is it allows you to decouple your actions from functions without attaching the actions to anything. You can simply create a store anywhere in your app and export the useStore and dispatch functions. Then you can dispatch from anywhere in your app.
well good approach but i still see redux better for larger application especially when come to performance. A example using your approach,is adding The button as separated component while wrapping it with React.memo and firing actions.toggle() from the button component, but the button re render 2 times which it doesn't relay on the changed state.
so when building big apps you are always looking for performance improvement by removing unnecessary re renders but this is not the case here.
this is my analyses, thanks for your work.
here the code showcase
I'm importing a plain class to my react (functional) component and want to be notified when an imported class property is set/updated. I've tried setting my imported class with just new, as a state variable with useState, as a ref with useRef - and have tried passing each one as a parameter to useEffect, but none of them are triggering the useEffect function when the property is updated a second time.
I've excluded all other code to drill down to the problem. I'm using Typescript, so my plain vanilla MyClass looks like this:
class MyClass {
userId: string
user: User?
constructor(userId: string){
this.userId = userId
// Do a network call to get the user
getUser().then(networkUser => {
// This works because I tried a callback here and can console.log the user
this.user = networkUser
}).catch(() => {})
}
}
And then in my component:
// React component
import { useEffect } from 'react'
import MyClass from './MyClass'
export default () => {
const myClass = new MyClass(userId)
console.log(myClass.user) // undefined here
useEffect(() => {
console.log(myClass.user) // undefined here and never called again after myClass.user is updated
}, [myClass.user])
return null
}
Again, this is greatly simplified. But the problem is that React is not re-rendering my component when the instance user object is updated from undefined to a User. This is all client side. How do I watch myClass.user in a way to trigger a re-render when it finally updates?
Let me guess you want to handle the business logic side of the app with OOP then relay the state back to functional React component to display.
You need a mechanism to notify React about the change. And the only way for React to be aware of a (view) state change is via a call to setState() somewhere.
The myth goes that React can react to props change, context change, state change. Fact is, props and context changes are just state change at a higher level.
Without further ado, I propose this solution, define a useWatch custom hook:
function useWatch(target, keys) {
const [__, updateChangeId] = useState(0)
// useMemo to prevent unnecessary calls
return useMemo(
() => {
const descriptor = keys.reduce((acc, key) => {
const internalKey = `##__${key}__`
acc[key] = {
enumerable: true,
configurable: true,
get() {
return target[internalKey]
},
set(value) {
if (target[internalKey] !== value) {
target[internalKey] = value
updateChangeId(id => id + 1) // <-- notify React about the change,
// the value's not important
}
}
}
return acc
}, {})
return Object.defineProperties(target, descriptor)
},
[target, ...keys]
)
}
Usage:
// React component
import { useEffect } from 'react'
import { useWatch } from './customHooks'
import MyClass from './MyClass'
export default () => {
const myClass = useMemo(() => new MyClass(userId), [userId])
useWatch(myClass, ['user'])
useEffect(() => {
console.log(myClass.user)
}, [myClass, myClass.user])
return null
}
Side Note
Not related to the question per se, but there're a few words I want to add about that myth I mentioned. I said:
props and context changes are just state change at a higher level
Examples:
props change:
function Mom() {
const [value, setValue] = useState(0)
setTimeout(() => setValue(v => v+1), 1000)
return <Kid value={value} />
}
function Dad() {
let value = 0
setTimeout(() => value++, 1000)
return <Kid value={value} />
}
function Kid(props) {
return `value: ${props.value}`
}
context change:
const Context = React.createContext(0)
function Mom() {
const [value, setValue] = useState(0)
setTimeout(() => setValue(v => v+1), 1000)
return (<Context.Provider value={value}>
<Kid />
</Context.Provider>)
}
function Dad() {
let value = 0
setTimeout(() => value++, 1000)
return (<Context.Provider value={value}>
<Kid />
</Context.Provider>)
}
function Kid() {
const value = React.useContext(Context)
return `value: ${value}`
}
In both examples, only <Mom /> can get <Kid /> to react to changes.
You can pass this.user as props and use props,.user in useEffeect. You could do that from the place getUser called.
A wholesome solution would be using a centralized state solution like redux or context API. Then you need to update store in getUser function and listen globalstate.user.
Conclusion
You need to pass this.user to the component one way or another. You need to choose according to the project.
I have a redux application with a "campaign" reducer/store.
Currently I have repeated code to check if a specific campaign is loaded or needs an API call to fetch details from the DB. Much simplified it looks like this:
// Reducer ----------
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
items: {... campaignList}
}
}
// Component ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Now I don't like to repeat the complex condition for needFetch. I also don't like to have this complex code in the mapStateToProps function at all, I want to have a simple check. So I came up with this solution:
// Reducer NEW ----------
const needFetch = (items) => (id) => { // <-- Added this function.
if (!items[id]) return true;
if (!items[id].meta) return true;
if (!items[id].meta.loaded && !items[id].meta.loading) return true;
return false;
}
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
needFetch: needFetch(campaignList), // <-- Added public access to the new function.
items: {... campaignList}
}
}
// Component NEW ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: state.campaign.needFetch(campaignId), // <-- Much simpler!
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Question: Is this a good solution, or does the redux-structure expect a different pattern to solve this?
Question 2: Should we add getter methods to the store, like store.campaign.getItem(myId) to add sanitation (make sure myId exists and is loaded, ..) or is there a different approach for this in redux?
Usually computational components should be responsible for doing this type of logic. Sure your function has a complex conditional check, it belongs exactly inside your computational component (just like the way you currently have it).
Also, redux is only for maintaining state. There's no reason to add methods to query values of the current state inside your reducers. A better way would be having a module specifically for parsing your state. You can then pass state to the module and it would extract the relevant info. Keep your redux/store code focused on computing a state only.
Your approach is somewhat against the idiomatic understanding of state in redux. You should keep only serializable data in the state, not functions. Otherwise you loose many of the benefits of redux, e.g. that you can very easily stash your application's state into the local storage or hydrate it from the server to resume previous sessions.
Instead, I would extract the condition into a separate library file and import it into the container component where necessary:
// needsFetch.js
export default function needsFetch(campaign) {
return campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading);
}
// Component ----------
import needsFetch from './needsFetch';
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: needsFetch(campaign),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);