Create a hook with a consumer (provider/consumer) - reactjs

How can I create a hook based on a consumer and provider?
The reason for why I am asking this is because the Provider and Consumer is exported from another library, so I can not extract that part more than that, and I would like a neat way to just add a value to the useHook to populate wanted values.
// I have a provider somewhere above this part, and here is the consumer:
<MyConsumer>
{props => <Values {...props} />}
</MyConsumer>
I would like to create a hook that returns the values I get inside the Values component. And maybe the best is to include the MyConsumer inside that hook so I would not need "two lines of code" where I want to use the hook.
I am imagining something like:
const useValues = (props) => {
const [state, setState] = useState({});
// do stuff
return state;
}
As mention, I do not know what I should do about the MyConsumer part.
Thank you for any help

You can do this by using your own context to wrap the provider and consumer.
const MyStateCtx = createContext();
const useMyState = () => useContext(MyStateCtx);
// LibraryProvider and LibraryConsumer are the provider and consumer
// supplied by the library you're using.
const MyStateProvider = ({ children }) => {
return (
<LibraryProvider>
<LibraryConsumer>
{(props) => (
<MyStateCtx.Provider value={props}>{children}</MyStateCtx.Provider>
)}
</LibraryConsumer>
</LibraryProvider>
);
};
const ConsumingComponent = () => {
// You can consume state here with this hook ✨
const myState = useMyState();
return <div />;
};
export default function App() {
return (
<MyStateProvider>
<div>
<ConsumingComponent />
</div>
</MyStateProvider>
);
}
Codesandbox link

Related

React (+ Typescript) component not rerendering upon updating Context

I have a LaunchItem component which uses React.Context to get and set information to/from the local storage.
What I am trying to achieve is that, when the component updates the Context (and local storage), I want it to rerender with the new information, so that it then updates the state of a local button.
The problem is, although the Context seems to be updated as well as the contents of the local storage, the item is not rerendered. (when I refresh the page I can see the button has changed state, however, signifying that it is able to derive that information from the Context just fine.
I will now share some code and hopefully someone is able to understand what I might be missing, I thoroughly appreciate your help :)
Context provider setup
type FavoritesContextType = {
favorites: Favorites;
updateFavorites: (category: StorageCategory, item: string) => void;
};
export const FavoritesContext = createContext<FavoritesContextType>(
{} as FavoritesContextType
);
const FavoritesProvider: FC = ({ children }) => {
const [favorites, setFavorites] = useState<Favorites>(
getFromLocalStorage(SOME_CONSTANT)
);
const updateFavorites = (category: StorageCategory, item: string) => {
updateLocalStorage(category, item);
setFavorites(favorites);
};
return (
<FavoritesContext.Provider value={{ favorites, updateFavorites }}>
{children}
</FavoritesContext.Provider>
);
};
export const useFavoritesContext = () => useContext(FavoritesContext);
App.tsx
export const App = () => {
return (
<FavoritesProvider>
{/* Some routing wrapper and a few routes each rendering a component */}
<Route path="/launches" element={<Launches />} />
</FavoritesProvider>
)
Launches.tsx
export const LaunchItem = ({ launch }: LaunchItemProps) => {
const { favorites, updateFavorites } = useFavoritesContext();
const [isFavorite, setIsFavorite] = useState(false);
useEffect(() => {
if (favorites) {
setIsFavorite(
favorites.launches.includes(launch.flight_number.toString())
);
}
}, [favorites]);
return (
{/* The rest of the component, irrelevant */}
<FavoriteButton
isFavorite={isFavorite}
updateFavorites={() => {
updateFavorites(
StorageCategory.Launches,
launch.flight_number.toString()
);
}}
/>
)
FavoriteButton.tsx
export const FavoriteButton = ({
isFavorite,
updateFavorites,
}: FavoriteButtonProps) => {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault();
updateFavorites();
};
return (
// Using Link vs a Button to be able to preventDefault of parent Link
<Link
onClick={handleClick}
>
{/* The rest of the component, irrelevant */}
It seems as though in your updateFavorites function you're calling setFavorites and passing in the existing favorites value. Try instead writing your updateFavorites function as:
const updateFavorites = (category: StorageCategory, item: string) => {
updateLocalStorage(category, item);
setFavorites(getFromLocalStorage(SOME_CONSTANT));
};
There are other ways you could determine what value to pass to setFavorites but I reused your getFromLocalStorage function as I'm not sure how you're determining that state value.
By doing it this way you'll ensure that the value you're setting in setFavorites isn't the same as the existing favorites value and thus you'll trigger a re-render.

How To Make All Consumers of The Context Able to Change Context State but Only Some of Them Will be Re-rendered [duplicate]

Whenever I update user (object) in users (array) in context - all components which uses users re-renders.
What I've tried
I have a component which is using values from context:
const DashboardCardList=()=> {
const context = useContext(StaticsContext);
const users = context.users.filter(user=>user.visible);
return !users
? <Loading/>
: (
<Container>
{users.map(user=>
<DashboardCard key={user._id} user={user}/>
)}
</Container>
);
};
My update function (updates context state):
const onUserUpdate=(user)=>{
const index = this.state.users.findIndex(item => item._id === user._id);
const users = [...this.state.users]
users[index] = user;
this.setState({users:users});
}
Final component:
const DashboardCard=({user})=> {
console.log("I'm here!", user);
return ...;
}
Question
Why it keeps re-rendering? Is it because of context?
How to write this properly?
There is no render bailout for context consumers (v17).
Here is a demonstration, where the Consumer will always re-render just because he is a Context Consumer, even though he doesn't consume anything.
import React, { useState, useContext, useMemo } from "react";
import ReactDOM from "react-dom";
// People wonder why the component gets render although the used value didn't change
const Context = React.createContext();
const Provider = ({ children }) => {
const [counter, setCounter] = useState(0);
const value = useMemo(() => {
const count = () => setCounter(p => p + 1);
return [counter, count];
}, [counter]);
return <Context.Provider value={value}>{children}</Context.Provider>;
};
const Consumer = React.memo(() => {
useContext(Context);
console.log("rendered");
return <>Consumer</>;
});
const ContextChanger = () => {
const [, count] = useContext(Context);
return <button onClick={count}>Count</button>;
};
const App = () => {
return (
<Provider>
<Consumer />
<ContextChanger />
</Provider>
);
};
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
To fix it:
Use a single context for each consumed value. Meaning that context holds a single value, (and no, there is no problem with multiple contexts in an application).
Use a state management solution like Recoil.js, Redux, MobX, etc. (although it might be overkill, think good about app design beforehand).
Minor optimization can be achieved by memoizing Provider's values with useMemo.

React To Many Re Renders In Context [duplicate]

Whenever I update user (object) in users (array) in context - all components which uses users re-renders.
What I've tried
I have a component which is using values from context:
const DashboardCardList=()=> {
const context = useContext(StaticsContext);
const users = context.users.filter(user=>user.visible);
return !users
? <Loading/>
: (
<Container>
{users.map(user=>
<DashboardCard key={user._id} user={user}/>
)}
</Container>
);
};
My update function (updates context state):
const onUserUpdate=(user)=>{
const index = this.state.users.findIndex(item => item._id === user._id);
const users = [...this.state.users]
users[index] = user;
this.setState({users:users});
}
Final component:
const DashboardCard=({user})=> {
console.log("I'm here!", user);
return ...;
}
Question
Why it keeps re-rendering? Is it because of context?
How to write this properly?
There is no render bailout for context consumers (v17).
Here is a demonstration, where the Consumer will always re-render just because he is a Context Consumer, even though he doesn't consume anything.
import React, { useState, useContext, useMemo } from "react";
import ReactDOM from "react-dom";
// People wonder why the component gets render although the used value didn't change
const Context = React.createContext();
const Provider = ({ children }) => {
const [counter, setCounter] = useState(0);
const value = useMemo(() => {
const count = () => setCounter(p => p + 1);
return [counter, count];
}, [counter]);
return <Context.Provider value={value}>{children}</Context.Provider>;
};
const Consumer = React.memo(() => {
useContext(Context);
console.log("rendered");
return <>Consumer</>;
});
const ContextChanger = () => {
const [, count] = useContext(Context);
return <button onClick={count}>Count</button>;
};
const App = () => {
return (
<Provider>
<Consumer />
<ContextChanger />
</Provider>
);
};
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
To fix it:
Use a single context for each consumed value. Meaning that context holds a single value, (and no, there is no problem with multiple contexts in an application).
Use a state management solution like Recoil.js, Redux, MobX, etc. (although it might be overkill, think good about app design beforehand).
Minor optimization can be achieved by memoizing Provider's values with useMemo.

Prevent context.consumer from re-rendering component

I have created the following context provider. In sort it's a toast generator. It can have multiple toasts visible at the same time.
It all worked great and such until I realized that the <Component/> further down the tree that called the const context = useContext(ToastContext) aka the consumer of this context and the creator of the toast notifications, was also re-rendering when the providerValue was changing.
I tried to prevent that, changing the useMemo to a useState hook for the providerValue, which did stop my re-rendering problem , but now I could only have 1 toast active at a time (because toasts was never updated inside the add function).
Is there a way to have both my scenarios?
export const withToastProvider = (Component) => {
const WithToastProvider = (props) => {
const [toasts, setToasts] = useState([])
const add = (toastSettings) => {
const id = generateUEID()
setToasts([...toasts, { id, toastSettings }])
}
const remove = (id) => setToasts(toasts.filter((t) => t.id !== id))
// const [providerValue] = useState({ add, remove })
const providerValue = React.useMemo(() => {
return { add, remove }
}, [toasts])
const renderToasts = toasts.map((t, index) => (
<ToastNote key={t.id} remove={() => remove(t.id)} {...t.toastSettings} />
))
return (
<ToastContext.Provider value={providerValue}>
<Component {...props} />
<ToastWrapper>{renderToasts}</ToastWrapper>
</ToastContext.Provider>
)
}
return WithToastProvider
}
Thank you #cbdeveloper, I figured it out.
The problem was not on my Context but on the caller. I needed to use a useMemo() there to have memoized the part of the component that didnt need to update.

Set React Context inside function-only component

My goal is very simple. I am just looking to set my react context from within a reusable function-only (stateless?) react component.
When this reusable function gets called it will set the context (state inside) to values i provide. The problem is of course you can't import react inside a function-only component and hence I cannot set the context throughout my app.
There's nothing really to show its a simple problem.
But just in case:
<button onCLick={() => PlaySong()}></button>
export function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //cannot call useContext in this component
}
If i use a regular react component, i cannot call this function onClick:
export default function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //fine
}
But:
<button onCLick={() => <PlaySong />}></button> //not an executable function
One solution: I know i can easily solve this problem by simply creating a Playbtn component and place that in every song so it plays the song. The problem with this approach is that i am using a react-player library so i cannot place a Playbtn component in there...
You're so close! You just need to define the callback inside the function component.
export const PlaySongButton = ({...props}) => {
const {setCurrentSong} = useContext(StoreContext);
const playSong = () => {
setCurrentSong("some song");
}
return (
<button
{...props}
onClick={() => playSong()}
/>
)
}
If you want greater re-usability, you can create custom hooks to consume your context. Of course where you use these still has to follow the rules of hooks.
export const useSetCurrentSong = (song) => {
const {setCurrentSong} = useContext(StoreContext);
setCurrentSong(song);
}
It is possible to trigger a hook function by rendering a component, but you cannot call a component like you are trying to do.
const PlaySong = () => {
const {setCurrentSong} = useContext(StoreContext);
useEffect( () => {
setCurrentSong("some song");
}, []
}
return null;
}
const MyComponent = () => {
const [shouldPlay, setShouldPlay] = useState(false);
return (
<>
<button onClick={() => setShouldPlay(true)}>Play</button>
{shouldPlay && <PlaySong />}
</>
)
}

Resources