React Hooks - Global state without context - javascript

I am implementing a hook "useUserPosts", which is supposed to be used in several routes of my application.
As I already have a context "PostsContext" which re-renders my Cards when data changes (totalLikes, totalComments, descriptions, ...), I have decided to avoid creating another one called "UserPostsContext" which purpose is to return the user posts array.
I know, why not to use the PostsContext instead?...
The answer is that, in PostsContext, to avoid performance issues, I am storing a map (key, value), in order to get/update the posts dynamic data in O(1), something which is only useful in my components (so, it is used to synchronize Cards basically)
Is it possible/a common practice in React to create hooks that handles global states without using the Context API or Redux?
I mean, something like
// Global State Hook
const useUserPosts = (() => {
const [posts, setPosts] = useState({});
return ((userId) => [posts[id] ?? [], setPosts]);
})();
// Using the Global State Hook
function useFetchUserPosts(userId) {
const [posts, setPosts] = useUserPosts(userId);
const [loading, setLoading] = useState(!posts.length);
const [error, setError] = useState(undefined);
const cursor = useRef(new Date());
const hasMoreToLoad = useRef(false);
const isFirstFetch = useRef(true);
const getUserPosts = async () => {
// ...
}
return { posts, loading, error, getUserPosts };
}
Note: my purpose with this is to:
1. Reproduce some kind of cache
2. Synchronize the fetched data of each stack screen that is mounted in order to reduce backend costs
3. Synchronize user posts deletions

Even if I think creating a new global state is the best solution, if you really want to avoid it, you could create your own as follow :
export class AppState {
private static _instance: AppState;
public state = new BehaviorSubject<AppStateType>({});
/**
* Set app state without erasing the previous values (values not available in the newState param)
* */
public setAppState = (newState: AppStateType) => {
this.state.next({ ...this.state, ...newState });
};
private constructor() {}
public static getInstance(): AppState {
if (!AppState._instance) {
AppState._instance = new AppState();
}
return AppState._instance;
}
}
With this kind of type :
export type AppStateType = {
username?: string;
isThingOk?: boolean;
arrayOfThing?: Array<MyType>;
...
}
And use it this way :
const appState = AppState.getInstance();
...
...
appState.setAppState({ isThingOk: data });
...
...
appState.state.subscribe((state: AppStateType) => {// do your thing here});
Not sure this is the best way to create a state of your own but it works pretty well. Feel free to adapt it to your needs.

I can recommand you to use some light state management library as zustand: https://github.com/pmndrs/zustand.
You can with this library avoid re-render and specify to re-render just want the data you want change or change in certain way with some function to compare old and new value.

Related

React - Running side effects inside pure functions

Introduction
My current use case requires to store the most fresh state updates in a cache. As state updates are async, and there can be a lot of components updating the same one in parallel, it might be a good option to store them inside the body of the useState or useReducer pure functions.
But... side effects come, and the frustration start. I have tried to await dispatches, creating custom hooks "useReducerWithCallback", and other stuff, but I don't see the correct solution to my problem.
Problem
I have a module usersCache.js which provides me with the necessary methods to make modifications to my cache:
const cache = {};
export const insert = (id, data) => ...
export const get = (id) => ...
// and more stuff
I am trying to update this cache when I make state updates. For example:
const currentUser = useContext(CurrentUserContext);
...
// Note: setData is just the state setter useState hook
currentUser.setData((prevData) => {
const newTotalFollowing = prevData.totalFollowing + 1;
usersCache.update(currentUser.data.id, { newTotalFollowing }); <---- SIDE EFFECT
return { ...prevData, totalFollowing: newTotalFollowing };
});
And same stuff in my otherUsers reducer
import { usersCache } from "../../services/firebase/api/users"
export default (otherUsers, action) => {
switch (action.type) {
case "follow-user": {
const { userId, isFollowing } = action;
const prevUserData = otherUsers.get(userId);
const newTotalFollowers = prevUserData.totalFollowers + (isFollowing ? 1 : -1);
usersCache.update(userId, { totalFollowers: newTotalFollowers }); // merge update
return new Map([
...otherUsers,
[
userId,
{
...prevUserData,
totalFollowers: newTotalFollowers
]
]
);
}
...
}
}
As in pure functions we shouldn't perform side effects... Is there any other approach to handle this?
Note: I am not using Redux
You can check this full working example using the repository pattern and react hooks to simplify async actions with state dispatches. I know you are not using redux but you can adapt this example using the useReducer hook to connect it to your React Context store.

Is it correct to use two different useState hook to store an array and a filtered array for passing to components?

I am currently porting my Pokemon project to React as I just learned the basics of React just a couple of weeks ago and want to see how well I can adapt it. Right now, the way I have my code architectured is to use two different useState hooks, one for the original array fetched from the PokeAPI and is only set once. This original array is also passed into my form object, which filters it according to a few form elements such as a Pokemon type or the Pokemon's name. And another useState hook is used to keep track of the filteredList which is what gets rendered to the website using the Array.map() function.
const [pokemonList, setPokemonList] = useState([]);
const [filteredList, setPokemonFilteredList] = useState([]);
Here's the useEffect() hook where we fetch and set the states
useEffect(() => {
const getPokemon = async () => {
const list = await fetchPokemon();
setPokemonList(list);
setPokemonFilteredList(list);
};
And finally the pokemonList state variable and setPokemonFilteredList methods get passed into the <PokemonSearchForm>
<PokemonSearchForm pokemonList={pokemonList} setPokemonList={setPokemonFilteredList} />
So as my question title suggests, is the way I use two different useState() 'correct'? Maybe a different way is for the child component to access pokemonList variable? But I believe this may be an anti-pattern.
It is better practice to not maintain duplicate or, in this case, derived state because you may run into divergence. For example, if your original pokemon data got updated, how would you make sure your filtered data got updated and then the filters re-applied? It gets hairy very fast.
The preferred alternative is to maintain the original data and filters in state and then compute the derived state (in this case, filter the list) during render.
function App() {
const [pokemonList, setPokemonList] = useState([]);
// Some default filter state
const [filters, setFilters] = useState({
types: ["any"],
search: ""
});
const filteredList = pokemonList.filter((pokemon) => {
// Filter logic here
});
return <PokemonSearchForm setFilters={setFilters} />
}
I would refactor this in a few different ways:
Keep the filter state in your parent component, the child component will simply notify it when those change.
Ditch the useState for useMemo which computes a value every time its dependencies change.
function YourComponent() {
const [filters, setFilters] = useState({});
const [pokemons, setPokemons] = useState([]);
useEffect(
() => {
const list = await fetchPokemon();
setPokemons(list);
},
[]
);
// This will run each time `filters` or `pokemons` change.
const filteredPokemons = useMemo(
() => {
return pokemons.filter((pokemon) => {
// Perform any filtering logic you may have,
// based on the filters set by the child component.
if (filters.name) {
return pokemon.name.includes(filters.name);
}
// etc...
});
},
[filters, pokemons]
);
return (
<PokemonSearchForm
pokemons={filteredPokemons}
onChange={setFilters}
/>
);
}

React context and nullable values in state

I'm trying to make a context in react that will hold some values as defined by an interface (Client in the example below). My problem is that it requires me to make that field nullable in the state interface (ClientState), meaning I would have to check for null values everywhere I consume the context. I want to avoid that.
Example code:
interface Client {
value1: string,
value2: number
}
interface ClientState {
client?: Client
}
const initialState: ClientState = {
client: undefined
}
const ClientContext = React.createContext<ClientState>(initialState);
export const useClient = (): ClientState => useContext(ClientContext);
export const EmployeeContextProvider = ({ children }: PropsWithChildren<{}>) => {
const [state, setState] = useState({});
// abstracted, not relevant to this problem
const loadFiles = () => {
setState(
{
value1: "test",
value2: 1
}
)
}
useEffect(() => loadFiles(), []);
return (
<ClientContext.Provider value={state}>
{children}
</ClientContext.Provider>
)
}
So far I've tried and deemed unsatisfactory:
Giving the client field in initialState a dummy object. The problem with this is that the real version of this has a large number of these fields, meaning lots of dummy code.
Adding a check to useClient() for members of the Client interface, same problem as 1.
Also of semi-relevance is that I don't need to modify this context beyond initialization and it's perfectly fine for it to be read-only.
client needs to be optional because its value is set from state which is initialised to an empty object. The type of state is inferred as ClientState.
useState<ClientState>({}) would require all properties of ClientState to be optional.
You could force TypeScript to accept an empty (dummy) object as if it complied with ClientState using useState({} as ClientState) but that means your Provider really will be providing an unusable client until setState has been invoked with a real one.
But that seems to be the problem you would prefer, over checking for null/undefined each time you wish to make use of the client...
TypeScript is perhaps saving you from yourself here. If your client really can be undefined then you should check it every time you use it!
I think this does what you want.
Your ClientState type seemed to serve only to allow the client to be undefined, which you said you did not want, so I have assumed you would rather not have this. Also, it was conflicting with your setState call where you are setting a Client rather than a ClientState.
This allows for a null Client in state, but NOT in the Context. A guard makes sure the Context and its children are not rendered until the client is set.
import React, { PropsWithChildren, useContext, useEffect, useState } from "react";
interface Client {
value1: string,
value2: number
}
// Note: ClientContext is initialised with an unusable Client object
const ClientContext = React.createContext<Client>({} as Client);
export const useClient = (): Client => useContext(ClientContext);
export const EmployeeContextProvider = ({ children }: PropsWithChildren<{}>) => {
// We allow state to be set to null
const [state, setState] = useState<Client | null>(null);
// abstracted, not relevant to this problem
const loadFiles = () => {
setState(
{
value1: "test",
value2: 1
}
)
}
useEffect(() => loadFiles(), []);
// Guard against null so that state can be provided as a Client
return (
state != null ?
<ClientContext.Provider value={state} >
{children}
</ClientContext.Provider>
: null
)
}

React hooks best practice storing refs to third party libraries

I'm creating a wrapper hooks library around pusher-js to publish into the wild. For each hooks (i.e. useChannel, usePresenceChannel, useTrigger), I need to keep a reference to the Pusher instance, i.e. new Pusher() stored in a Context Provider. I am allowing third party auth to be passed in so I need to create the Pusher instance on the fly. I'm unsure whether I should be storing this in a useState or useRef.
The eslint-plugin-react-hooks rules complain with various combinations of useState and useRef to store this. I'm also seeing undesirable side effects when trying to clean up correctly with each. I'm unsure what's considered best practice.
Here's a simplified implementation with the important details. See comments 1. 2. and 3. below for my questions.
// PusherContext.jsx
export const PusherContext = React.createContext();
export function PusherProvider({key, config, ...props}){
// 1. Should I store third party libraries like this?
const clientRef = useRef();
// vs const [client, setClient] = useState();
// when config changes, i.e. config.auth, re-create instance
useEffect(() => {
clientRef.current && clientRef.current.disconnect();
clientRef.current = new Pusher(key, {...config});
}, [clientRef, config]);
return <PusherContext.Provider value={{ client }} {...props} />
}
// useChannel.js
export function useChannel(
channelName,
eventName,
callback,
callbackDeps
){
const { client } = useContext(PusherContext);
const callbackRef = useCallback(callback, callbackDeps);
// 2. Or should I be using state instead?
const [channel, setChannel] = useState();
// vs. const channelRef = useRef();
useEffect(() => {
if(client.current){
const pusherChannel = client.current.subscribe(channelName);
pusherChannel.bind(eventName, callbackRef.current);
setChannel(pusherChannel);
}
// can't cleanup here because callbackRef changes often.
// 3. eslint: Mutable values like 'client.current' aren't valid dependencies because mutating them doesn't re-render the component
}, [client.current, callbackRef])
// cleanup for unbinding the event
// re-bind makes sense with an updated callback ref
useEffect(() => {
channel.unbind(eventName)
}, [client, channel, callbackRef, eventName]);
// cleanup for unsubscribing from the channel
useEffect(() => {
clientRef.unsubscribe(channelName);
}, [client, channelName])
}
Any advice, past examples or patterns are greatly appreciated as I want to nail this one!
I'd use the ref to hold a new instance of Pusher as recommended by Dan.
You don't need to clean up initially by doing null check and disconnect (clientRef.current && clientRef.current.disconnect()) in the inside effect because whenever the useEffect is run, React will disconnect when you handle it in the return statement.
export function PusherProvider({ key, config, ...props }) {
// 1. Should I store third party libraries like this?
const clientRef = useRef();
// vs const [client, setClient] = useState();
// when config changes, i.e. config.auth, re-create instance
// useEffect(() => {
// clientRef.current && clientRef.current.disconnect();
// clientRef.current = new Pusher(key, { ...config });
// }, [clientRef, config]);
// Create an instance, and disconnect on the next render
// whenever clientRef or config changes.
useEffect(() => {
clientRef.current = new Pusher(key, { ...config });
// React will take care of disconnect on next effect run.
return () => clientRef.current.disconnect();
}, [clientRef, config]);
return <PusherContext.Provider value={{ client }} {...props} />;
}
For the second case, I tried as much to write suggestions in-line below.
The gist is that, un/subscription are related events, thus should be handled in the same effect (as it was the case for PusherProvider).
// useChannel.js
export function useChannel(channelName, eventName, callback, callbackDeps) {
const { client } = useContext(PusherContext);
const callbackRef = useCallback(callback, callbackDeps);
// 2. Or should I be using state instead?
// I believe a state makes sense as un/subscription depends on the channel name.
// And it's easier to trigger the effect using deps array below.
const [channel, setChannel] = useState();
useEffect(() => {
// do not run the effect if we don't have the Pusher available.
if (!client.current) return;
const pusherChannel = client.current.subscribe(channelName);
pusherChannel.bind(eventName, callbackRef.current);
setChannel(pusherChannel);
// Co-locate the concern by letting React
// to take care of un/subscription on each channel name changes
return () => client.current.unsubscribe(channelName);
// Added `channelName` in the list as the un/subscription occurs on channel name changes.
}, [client.current, callbackRef, channelName]);
// This.. I am not sure... 🤔
// cleanup for unbinding the event
// re-bind makes sense with an updated callback ref
useEffect(() => {
channel.unbind(eventName);
}, [client, channel, callbackRef, eventName]);
// Moved it up to the first `useEffect` to co-locate the logic
// // cleanup for unsubscribing from the channel
// useEffect(() => {
// clientRef.unsubscribe(channelName);
// }, [client, channelName]);
}

Alternative to Redux reducer

I'm building a library that other apps will use. My library is an extension to redux.
To keep this question as general as possible, I currently have middlewares, action creators and one reducer.
The reducer is the problem because my reducer depends on the state structure which I, as a library developer, can't and shouldn't know. The user may use a combineReducers function or other function and give my reducer's state any name he wants.
My question is - What options Redux.js library provide to library developers in a case like this; hiding reducers/other alternatives to reducers?
Wrap your library in a configuration function, which requires the user to supply a selector, that points to place in the state that your reducer occupies.
In addition, if users access your state (not the case for you right now), you can supply selectors to use the state, without knowing it's structure.
A general non-working example:
const createSelectors = (mainSelector) => ({
selectorA: (state) => mainSelector(state).dataA,
selectorB: (state) => mainSelector(state).dataB,
});
const createMiddlewares = (actionTypes, selectors) => {
const middlewareA = ({ getState, dispatch }) =>
next => action => {
const myStateA = selectors.selectorA(getState());
};
return {
middlewareA
};
};
const factory = (mainSelector = ({ myState }) => myState) => {
const actionTypes = {};
const actions = {};
const reducer = () => {};
const selectors = createSelectors(mainSelector);
const middleware = createMiddlewares(actionTypes, selectors);
return {
actionTypes,
actions,
reducer,
middleware,
selectors
};
};
This package minimize needs for writing reducers, because it is using auto merge feature with actions. There is some solution with arrays too.
https://www.npmjs.com/package/micro-reducers
I think instead of struggling with the complex Redux library. you should give it a try with https://rootzjs.org/ an alternative to redux. Its literally like a cakewalk.

Categories