Add logic to the store? - javascript

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);

Related

How to subscribe on updates within ReactReduxContext.Consumer?

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

How can I update every React components using a context without provider?

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

React global state no context or redux?

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

Creating a var referencing a store from redux

I am currently working on creating a var that references a store from redux. I created one but within the render(). I want to avoid that and have it called outside of the render. Here is an example of it. I was recommended on using componentWillMount(), but I am not sure how to use it. Here is a snippet of the code I implemented. Note: It works, but only when I render the data. I am using double JSON.parse since they are strings with \
render() {
var busData= store.getState().bus.bus;
var driverData= store.getState().driver.gdriveras;
var dataReady = false;
if (busData&& driverData) {
dataReady = true;
console.log("========Parsing bus data waterout========");
var bus_data_json = JSON.parse(JSON.parse(busData));
console.log(bus_data_json);
console.log("========Parsing driver data waterout========");
var driver_data_json = JSON.parse(JSON.parse(driverData));
console.log(driver_datat_json);
busDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
driverDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
...
}
}
Here is an example of react-redux usage that will probably help you.
Don't forget to add StoreProvider to your top three component (often named App).
I warned you about the fact that React and Redux are not meant to be used by beginner javascript developer. You should consider learn about immutability and functional programming.
// ----
const driverReducer = (state, action) => {
switch (action.type) {
// ...
case 'SET_BUS': // I assume the action type
return {
...state,
gdriveras: JSON.parse(action.gdriveras) // parse your data here or even better: when you get the response
}
// ...
}
}
// same for busReducer (or where you get the bus HTTP response)
// you can also format your time properties when you get the HTTP response
// In some other file (YourComponent.js)
class YourComponent extends Component {
render() {
const {
bus,
drivers
} = this.props
if (!bus || !drivers) {
return 'loading...'
}
const formatedBus = bus.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
const formatedDrivers = drivers.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
// return children
}
}
// this add bus & drivers as props to your component
const mapStateToProps = state => ({
bus: state.bus.bus,
drivers: state.driver.gdriveras
})
export default connect(mapStateToProps)(YourComponent)
// you have to add StoreProvider from react-redux, otherwise connect will not be aware of your store

How To Update Structure of Redux State Persisted In LocalStorage

I save a part of my Redux state in localStorage so that I can persist it across page refreshes.
How do I update the structure of the state in my localStorage if my Redux state structure changes?
Example:
This is my combineReducer file:
// combineReducer
const rootReducer = combineReducers({
usersPage: usersPageReducer,
form: formReducer
})
The original default state in my usersPageReducer looks like:
// usersPageReducer.js
defaultUsersPageState = {
selectedColumns: ["name", "email"],
}
I load and save to localStorage using the following methods:
// localStorage.js
export const loadState = () => {
try {
const serializedState = localStorage.getItem('state')
if (serializedState === null) {
return undefined;
}
return JSON.parse(serializedState)
} catch (err) {
return undefined
}
}
export const saveState = (state) => {
try {
const serializedState = JSON.stringify(state)
localStorage.setItem('state', serializedState)
} catch (err) {
// to define
}
}
In my main file, I subscribe to the store, and save the usersPage slice of the state. And I create the store using initial state from localStorage:
// main.js
const persistedState = loadState();
const initialState = { ...persistedState };
const store = createStore(
rootReducer,
initialState,
);
store.subscribe(() => {
saveState({
usersPage: store.getState().usersPage
})
})
Now imagine I want to change my usersPageReducer default state so that it also keeps track of sorting.
// usersPageReducer.js
defaultUsersPageState = {
selectedColumns: ["name", "email"],
sorting: {"name": "asc"}
}
The above won't work because at the start of the app, we hydrate the redux initial state from localStorage. The old initial state beats out the usersPageReducer defaultUsersPageState.
Since localStorage doesn't know about the sorting key for usersPageReducer, and we never use the default state, the sorting key is never added to the redux state.
Is there a way to hydrate the redux store so that it initializes every reducer using it's default state (as if we weren't using localStorage), then spreads the stored state?
There are two possible solution I can suggest you:-
1. Use a variable to keep track of the version of your application (even better redux-version ). Now whenever value of this variable is changed, your application will clear-out the user's LocalStorage and will use the brand new structure of your redux ( or its sub-part ) provided by defaultUsersPageState rather than loading the available ( stale ) data from it.
You can achieve this in multiple ways, mine is
// localStorage.js
export const loadState = (CurrVersion) => {
try {
// check if version matches or not ...
// for first time localStorage.getItem('appVersion') will be null
if (CurrVersion !== localStorage.getItem('appVersion')){
localStorage.removeItem('state') //
// update the appVersion in LS with current version
localStorage.setItem('appVersion', CurrVersion)
return undefined;
}
else{
const serializedState = localStorage.getItem('state')
if (serializedState === null) {
return undefined;
}
return JSON.parse(serializedState)
}
} catch (err) {
return undefined
}
}
And in main.js
// change it whenever breaking changes are added and it makes sense
const CurrVersion = 'MY-APP-VERSION-2.0'
const persistedState = loadState(CurrVersion);
const initialState = { ...persistedState };
const store = createStore(
rootReducer,
initialState,
);
store.subscribe(() => {
saveState({
usersPage: store.getState().usersPage
})
})
Check explicitly if the structure of the persisted data is changed or not. If changed, you can delete the former object and use the default initial state provided which has updated structure. But keep in mind that this check needs to done recursively (or deeply), you may find lodash useful for this.
P.S : Use npm packages built for this problem e.g react-persist if you have big projects .
The hash method can be used to implement this.
Compute the hash of the initial redux keys on the application load and store it in the local storage of the browser
Now calculate the hash of the redux keys present in the application and compare it with the hash present in the browser's local storage.
If they both match, then no action needs to be taken.
If they don't, then either log them out or update the current redux store in the local storage
sha256 hash can be used to accomplish this
Example
import { sha256 } from "js-sha256";
const hash = sha256(digest);
INPUT
{"auth":"{\"email\":null,\"token\":null,\"error\":null,\"loading\":false,\"authRedirectPath\":\"/dashboard\",\"role\":null,\"clientId\":null,\"registerMessage\":null,\"signupCreds\":{}}
OUTPUT
29e9cceee4e2df4c53d134dedd0d171939caaab412b17332b3d864693dc62780

Categories

Resources