React Custom Hooks fetch data globally and share across components? - reactjs

in this react example from https://reactjs.org/docs/hooks-custom.html, a custom hook is used in 2 different components to fetch online status of a user...
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
then its used in the 2 functions below:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
my question is, will the function be executed individually everywhere where it is imported into a component? Or is there some thing like sharing the state between components, if it is defined as a separate exported function? e.g I execute the function only once, and the "isOnline" state is the same in all components?
And if its individually fetched, how would I have to do it to fetch data only once globally, and then pass it to different components in my React app?

To share state data across multiple components in a large project, I recommend to use Redux or React Context.
Nevertheless, you can implement a global isOnline state using the Observer pattern (https://en.wikipedia.org/wiki/Observer_pattern):
// file: isOnline.tsx
import { useEffect, useState } from 'react';
// use global variables
let isOnline = false;
let observers: React.Dispatch<React.SetStateAction<boolean>>[] = [];
// changes global isOnline state and updates all observers
export const setIsOnline = (online: boolean) => {
isOnline = online;
observers.forEach((update) => update(isOnline));
};
// React Hook
export const useIsOnline = (): [boolean, (online: boolean) => void] => {
const [isOnlineState, setIsOnlineState] = useState<boolean>(isOnline);
useEffect(() => {
// add setIsOnlineState to observers list
observers.push(setIsOnlineState);
// update isOnlineState with latest global isOnline state
setIsOnlineState(isOnline);
// remove this setIsOnlineState from observers, when component unmounts
return () => {
observers = observers.filter((update) => update !== setIsOnlineState);
};
}, []);
// return global isOnline state and setter function
return [isOnlineState, setIsOnline];
};
import { useIsOnline } from './isOnline';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useIsOnline();
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
function FriendStatus(props) {
const isOnline = useIsOnline()[0];
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useIsOnline()[0];
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
Edit: Inside useIsOnline return isOnlineState created with useState instead of the isOnline variable because otherwise React can't pass the changes down to the component.
Edit 2: Make useEffect independent of isOnlineState so the hook does not unsubscribe and resubscribe on each variable change.

In the case you mention, the function is executed at every component's render. So each component keeps a state value independently from the others. For this specific example it's what I would probably use.
If you need some state data to be shared globally (like authentication status), or between several components at different levels in DOM tree, one option is to use the React context.
First you define a new Context, by using the React.createContext() function.
Check this link for more info: https://reactjs.org/docs/context.html
Then, you must use the Context.Provider (a component which keep context value and manages the updates) at top of your DOM hierarchy and then you can use the hook useContext() to refer to context value (provided from the Context provider) in descendant components at any level.
Check this link for that:
https://reactjs.org/docs/hooks-reference.html#usecontext

You can use this library to convert any custom hook into singleton https://www.npmjs.com/package/react-singleton-hook .
This library creates a wrapper around your custom hook. The original hook is mounted only once into a hidden component. Other components and custom hooks consume wrapper and it delegates calls into your hook.
//useFriendStatusGlobal is a custom hook with globally shared data
const useFriendStatusGlobal = singletonHook(null, useFriendStatus);

Whenever you use a custom hook, there will be separate instances of the hook within your App and they won't share the data unless you are using context API within them which is common across multiple instances or your ChatAPI holds data in one place eg in a singleton class instance or within browserStorage/using API.
useState or useReducers will have separate instances within your App.
You can simply think of this as useState and useEffect being written multiple times within your code app in individual component

As others have mentioned, you need to have a state management in your React app. Context is not recommended for that: It is designed to avoid prop drilling to mid-components, not to be a state management library. All components that consume the context gets re-rendered when it updates, and this behavior is inefficient for a state management.
Redux might be a choice, but sometimes it brings to much boilerplate or concepts.
For this, I would recommend you Recoil, a simple state management library.
In Recoil, you have outsourced your state. Each piece of state is called an Atom. Then you bring the atom and its setState function by using the useRecoilState. Pretty straightforward if you already know how to use hooks.
Give a look at this example:
import React from 'react';
import {
RecoilRoot,
atom,
useRecoilState
} from 'recoil';
const isOnlineState = atom(null);
function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}
Then, when you try to fetch the friends:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useRecoilState(isOnlineState);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
Now you can bring it anywhere:
const [isOnline, setIsOnline] = useRecoilState(isOnlineState)

Related

How to re-render a component when a non state object is updated

I have an object which value updates and i would like to know if there is a way to re-render the component when my object value is updated.
I can't create a state object because the state won't be updated whenever the object is.
Using a ref is not a good idea(i think) since it does not cause a re-render when updated.
The said object is an instance of https://docs.kuzzle.io/sdk/js/7/core-classes/observer/introduction/
The observer class doesn't seem to play well with your use case since it's just sugar syntax to manage the updates with mutable objects. The documentation already has a section for React, and I suggest following that approach instead and using the SDK directly to retrieve the document by observing it.
You can implement this hook-observer pattern
import React, { useCallback, useEffect, useState } from "react";
import kuzzle from "./services/kuzzle";
const YourComponent = () => {
const [doc, setDoc] = useState({});
const initialize = useCallback(async () => {
await kuzzle.connect();
await kuzzle.realtime.subscribe(
"index",
"collection",
{ ids: ["document-id"] },
(notification) => {
if (notification.type !== "document" && notification.event !== "write")
return;
// getDocFromNotification will have logic to retrieve the doc from response
setDoc(getDocFromNotification(notification));
}
);
}, []);
useEffect(() => {
initialize();
return () => {
// clean up
if (kuzzle.connected) kuzzle.disconnect();
};
}, []);
return <div>{JSON.stringify(doc)}</div>;
};
useSyncExternalStore, a new React library hook, is what I believe to be the best choice.
StackBlitz TypeScript example
In your case, a simple store for "non state object" is made:
function createStore(initialState) {
const callbacks = new Set();
let state = initialState;
// subscribe
const subscribe = (cb) => {
callbacks.add(cb);
return () => callbacks.delete(cb);
};
// getSnapshot
const getSnapshot = () => state;
// setState
const setState = (fn) => {
state = fn(state);
callbacks.forEach((cb) => cb());
};
return { subscribe, getSnapshot, setState };
}
const store = createStore(initialPostData);
useSyncExternalStore handles the job when the update of "non state object" is performed:
const title = React.useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().title
);
In the example updatePostDataStore function get fake json data from JSONPlaceholder:
async function updatePostDataStore(store) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${Math.floor(Math.random()*100)+1}`)
const postData = await response.json()
store.setState((prev)=>({...prev,...postData}));
};
My answer assumes that the object cannot for some reason be in React as state (too big, too slow, too whatever). In most cases that's probably a wrong assumption, but it can happen.
I can't create a state object because the state won't be updated whenever the object is
I assume you mean you can't put that object in a React state. We could however put something else in state whenever we want an update. It's the easiest way to trigger a render in React.
Write a function instead of accessing the object directly. That way you can intercept every call that modifies the object. If you can reliably run an observer function when the object changes, that would work too.
Whatever you do, you can't get around calling a function that does something like useState to trigger a render. And you'll have to call it in some way every time you're modifying the object.
const myObject = {};
let i = 0;
let updater = null;
function setMyObject(key, value) {
myObject[key] = value;
i++;
if (updater !== null) {
updater(i);
}
};
Change your code to access the object only with setMyObject(key, value).
You could then put that in a hook. For simplicity I'll assume there's just 1 such object ever on the page.
function useCustomUpdater() {
const [, setState] = useState(0);
useEffect(()=>{
updater = setState;
return () => {
updater = null;
}
}, [setState]);
}
function MyComponent() {
useCustomUpdater();
return <div>I re-render when that object changes</div>;
}
Similarly, as long as you have control over the code that interacts with this object, you could wrap every such call with a function that also schedules an update.
Then, as long as your code properly calls the function, your component will get re-rendered. The only additional state is a single integer.
The question currently lacks too much detail to give a good assessment whether my suggested approach makes sense. But it seems like a very simple way to achieve what you describe.
It would be interesting to get more information about what kind of object it is, how frequently it's updated, and in which scope it lives.

Update React context from child component

I am not able to update my context object from child component. Here is my provider file for creating react context and that component is wrapped over whole application in root app.tsx file.
I can fetch context in child components but I am not sure how to update it.
const CorporateContext = React.createContext(undefined);
export const useCorporateContext = () => {
return useContext(CorporateContext);
};
export const CorporateProvider = ({ children }) => {
const [corporateUserData, setCorporateUserData] = useState(undefined);
useEffect(() => {
const localStorageSet = !!localStorage.getItem('corporateDetailsSet');
if (localStorageSet) {
setCorporateUserData({corporateId: localStorage.getItem('corporateId'), corporateRole: localStorage.getItem('corporateRole'), corporateAdmin: localStorage.getItem('corporateAdmin'), corporateSuperAdmin: localStorage.getItem('corporateSuperAdmin')});
} else {
(async () => {
try {
const json = await
fetchCorporateUserDetails(getClientSideJwtTokenCookie());
if (json.success !== true) {
console.log('json invalid');
return;
}
setCorporateUserData({corporateId: json.data.corporate_id, corporateRole: json.data.corporate_role, corporateAdmin: json.data.corporate_role == 'Admin', corporateSuperAdmin: json.data.corporate_super_admin});
addCorporateDetailsToLocalStorage(corporateUserData);
} catch (e) {
console.log(e.message);
}
})();
}
}, []);
return (
<CorporateContext.Provider value={corporateUserData}>
{children}
</CorporateContext.Provider>
);
};
export default CorporateProvider;
Update: Here is what I changed and context seems to be updated (at least it looks updated when I check in chrome devtools) but child components are not re-rendered and everything still stays the same:
I changed react context to:
const CorporateContext = React.createContext({
corporateContext: undefined,
setCorporateContext: (corporateContext) => {}
});
Changed useState hook in a component:
const [corporateContext, setCorporateContext] = useState(undefined);
Passed setState as property to context:
<CorporateContext.Provider value={{corporateContext , setCorporateContext}}>
{children}
</CorporateContext.Provider>
You can also pass in setCorporateUserData as a second value to the provider component as such:
//...
return (
<CorporateContext.Provider value={{corporateUserData, setCorporateUserData}}>
{children}
</CorporateContext.Provider>
);
You can then destructure it off anytime you want using the useContext hook.
How I resolved this issue:
Problem was that I have cloned corporateContext object and changed property on that cloned object and then I passed it to setCorporateContext() method. As a result of that, context was updated but react didnt noticed that change and child components were not updated.
let newCorporateContext = corporateContext;
newCorporateContext.property = newValue;
setCorporateContext(newCorporateContext);
After that I used javascript spread operator to change the property of corporateContext inside setCorporateContext() method.
setCorporateContext({...corporateContext, property: newValue);
After that, react noticed change in hook and child components are rerendered!

Best practice for marking hooks as not to be reused in multiple places

It seems a lot of my custom React Hooks don't work well, or seem to cause a big performance overhead if they are reused in multiple places. For example:
A hook that is only called in the context provider and sets up some context state/setters for the rest of the app to use
A hook that should only be called in a root component of a Route to setup some default state for the page
A hook that checks if a resource is cached and if not, retrieves it from the backend
Is there any way to ensure that a hook is only referenced once in a stack? Eg. I would like to trigger a warning or error when I call this hook in multiple components in the same cycle.
Alternatively, is there a pattern that I should use that simply prevents it being a problem to reuse such hooks?
Example of hook that should not be reused (third example). If I would use this hook in multiple places, I would most likely end up making unnecessary API calls.
export function useFetchIfNotCached({id}) {
const {apiResources} = useContext(AppContext);
useEffect(() => {
if (!apiResources[id]) {
fetchApiResource(id); // sets result into apiResources
}
}, [apiResources]);
return apiResources[id];
}
Example of what I want to prevent (please don't point out that this is a contrived example, I know, it's just to illustrate the problem):
export function Parent({id}) {
const resource = useFetchIfNotCached({id});
return <Child id={id}>{resource.Name}</Child>
}
export function Child({id}) {
const resource = useFetchIfNotCached({id}); // <--- should not be allowed
return <div>Child: {resource.Name}</div>
}
You need to transform your custom hooks into singleton stores, and subscribe to them directly from any component.
See reusable library implementation.
const Comp1 = () => {
const something = useCounter(); // is a singleton
}
const Comp2 = () => {
const something = useCounter(); // same something, no reset
}
To ensure that a hook called only once, you only need to add a state for it.
const useCustomHook = () => {
const [isCalled, setIsCalled] = useState(false);
// Your hook logic
const [state, setState] = useState(null);
const onSetState = (value) => {
setIsCalled(true);
setState(value);
};
return { state, setState: onSetState, isCalled };
};
Edit:
If you introduce a global variable in your custom hook you will get the expected result. Thats because global variables are not tied to component's lifecycle
let isCalledOnce = false;
const useCustomHook = () => {
// Your hook logic
const [state, setState] = useState(null);
const onSetState = (value) => {
if (!isCalledOnce) {
isCalledOnce = true;
setState(false);
}
};
return { state, setState: onSetState, isCalled };
};

React Hook - useCustomHook to set outside useState and useRef

I have the main component along with local state using useState and useRef, I also another custom hook, inside the custom hook I would like to reset my main component's state and ref, am I doing correctly by below?
// custom hook
const useLoadData = ({startLoad, setStartLoad, setLoadCompleted, setUserNameRef}) => {
useEffect(() => {
const fetchUser = async() => { await fetchFromApi(...); return userName;};
if (startLoad) {
const newUserName = fetchUser();
setStartLoad(false);
setLoadCompleted(true);
setUserNameRef(newUserName);
}
}, [startLoad]);
}
// main component
const myMainComp = () {
const [startLoad, setStartLoad] = useState(false);
const [loadCompleted, setLoadCompleted] = useState(false);
const userNameRef = useRef("");
const setUserNameRef = (username) => { this.userNameRef.current = username; }
useLoadData(startLoad, setStartLoad, setLoadCompleted, setUserNameRef);
refreshPage = (userId) => {
setStartLoad(true);
}
}
Am I using the custom hook correctly like by passing all external state value and setState method in? Also I found even I don't use useEffect in my customHook, it also works as expected, so do I need to use useEffect in my custom hook? Any review and suggestion is welcome!
First, I think isn't a good approach you use component methods inside custom hook (like "set" methods provided by useState). You are binding the hook with the main component's internal logic. If the purpose of custom hook is fetch data from API, it need to provide to main component the vars that can be able the main component to manipulate its state by itself (like return isFetching, error, data, etc. and don't call any main component set method inside hook).

Best practice to prevent state update warning for unmounted component from a handler

It is a common use-case to fetch and display the data from an external API (by using XHR requests) when a certain UI component (e.g. a <button />) is clicked. However, if the component was unmounted in the meantime, the following warning appears in the console:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
In fact, the most common solution (approved by #dan-abramov) to avoid the warning seems to keep track of the mount state of the component by using the return function of useEffect to cleanup.
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
export default function PhotoList() {
const mounted = useRef(true);
const [photos, setPhotos] = useState(null);
useEffect(() => {
return () => {
mounted.current = false;
};
}, []);
function handleLoadPhotos() {
axios("https://jsonplaceholder.typicode.com/photos").then(res => {
if (mounted.current) {
setPhotos(res.data);
}
});
}
return (
<div>
<button onClick={handleLoadPhotos}>Load photos</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
However, this seems to cause unnecessary overhead to keep track of the mounting state and to check it before every state update. This becomes especially obvious when Observables (where you can unsubscribe) instead of Promises are used.
While you indeed can unsubscribe inside of the useEffect using the cleanup function in a very neat way:
useEffect(() => {
// getPhotos() returns an observable of the photo list
const photos$ = getPhotos().subscribe(setPhotos);
return () => photos$.unsubscribe();
}, []);
The same smart cleanup is not possible within a handler:
function handleLoadPhotos() {
const photos$ = getPhotos().subscribe(setPhotos);
// how to unsubscribe on unmounting?
}
Is there a best practice to avoid the warning without the ugly manual tracking of the mounting state with useRef()? Are there good approaches for that when using Observables?
Problem is that you are trying to fetch data in your component. This is not a good idea since the component could be unmounted and you would face many possible errors.
So that, you should look for other ways.
I always do async operations in redux thunks.
You should avoid your approach. Use redux and redux-thunk if you like. If not, try to find another solution to move async operations outside of your components.
In fact, you should be writing declarative ui components which renders for given props. So that, your data should be outside of your components logic too.
That's an awesome question! This is how I would do it:
First, define a helper function (it's not cheating because it really is a highly reusable function whenever you're dealing with React and observables combined):
import * as React from 'react';
import { Observable } from 'rxjs';
export const useObservable = <Value>(
arg: () => {
observable: Observable<Value>;
value: Value;
},
) => {
const { observable, value } = React.useMemo(arg, []);
const [state, setState] = React.useState<Value>(value);
React.useEffect(() => {
const subscription = observable.subscribe(value => setState(value));
return () => subscription.unsubscribe();
}, []);
return state;
};
Just to help illustrate what this function does, the following component will display the latest value emitted by myObservable:
() => {
const value = useObservable(() => ({
observable: myObservable,
value: 'Nothing emitted yet',
}));
return <span>{value}</span>;
};
Your component will then look like this:
export default function PhotoList() {
const clicksSubject = React.useMemo(() => new Subject<undefined>(), []);
const photos = useObservable(() => ({
observable: clicksSubject.pipe(
switchMap(() => axiosApiCallReturningAnObservable()),
map(res => res.data),
),
value: null,
}));
return (
<div>
<button
onClick={() => {
clicksSubject.next(undefined);
}}
>
Load photos
</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
When the component is dismounted, useObservable unsubs from the observable that was passed to it. This makes sure that we don't at a later point attempt to set the state, and that the data fetching API aborts (or at least gets a chance to abort) the HTTP request.

Resources