hook change state doesn't update context provider's value? - javascript

I have 2 component, and a context provider, when I call my hook at parent level, I have no issue changing the state and having those 2 component getting the value via context
working demo of contex api usage but I call change state at parent level which is not what I wanted
https://stackblitz.com/edit/react-51e2ky?file=index.js
I want to change state at inner component with hook, but I don't see the value been changed when I click on the navbar login.
https://stackblitz.com/edit/react-rgenmi?file=Navbar.js
parent:
const App = () => {
const {user} = loginHook()
return (
<UserContext.Provider value={user}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
Navbar.js
const Navbar = () => {
const user = React.useContext(userContex)
const {setUser} = loginHook()
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
custom hook
const loginHook = () => {
const [user, setUser] = React.useState(null)
return {
user,
setUser
}
}
I can pass setUser from parent to children but I want to avoid that, I expect I can use context api and react hook seamlessly.

Currently, you're only setting the user value in the context, which is why getting the correct value will work.
However, in your Navbar.js component, you are making a call to loginHook, which will create a new "instance" of that hook, effectively having its own state.
I suggest you add the update function in your context as well, as such
const App = () => {
const {user, setUser} = loginHook()
return (
<UserContext.Provider value={{ user, setUser}}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
That way you can access the setUser in your children as well, e.g.
const Navbar = () => {
const {user, setUser} = React.useContext(userContex)
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
Also, small note: it's best to start you custom hook with use, as that's a best-practice when writing your own hooks.
Important caveat however, this is not really a good practice. If your user were to change, all components that are only listening to setUser will also get an update an thus do a useless rerender. You can solve this by using two different contexts, one for the value, and one for the updater. You can read more about this here

You cannot change the parent's context information from the child, no. You'll need to pass something to the child from the parent that the child can use to let the parent know that the context needs to be updated (such as the parent's copy of setUser). You can do that via a prop or by adding setUser to the context, though I'd lean toward just doing it as a prop to components that need to be able to set the user, rather than context they all have access to.
The reason using loginHook in both places didn't work is that each component (App and Navbar) has its own copy of user. This is fundamental to how hooks work. (If it somehow made them share the state information, useState wouldn't work at all — all state would be shared across all components.) Dan Abramov's A Complete Guide to useEffect may be a helpful read (it's more about how hooks and components work than it is specifically about useEffect).

You must note that custom hooks do not share instance references, so if you use the loginHook in App and another one in Navbar, they will create 2 separate states and updaters
Now using a setter from custom hook will now update the state in context.
You can restructure this by writing your loginHook so that it internally uses context and then using it
const App = () => {
const [user, setUser] = useState();
return (
<UserContext.Provider value={{user, setUser}}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
const Navbar = () => {
const {user, setUser} = loginHook();
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
const loginHook = () => {
const {user, setUser} = React.useContext(UserContext)
return {
user,
setUser
}
}
Now there are multiple ways to write this code, However the best way in the above scenario is not use a custom hook at all since it anyway is not useful
const App = () => {
const [user, setUser] = useState();
return (
<UserContext.Provider value={{user, setUser}}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
const Navbar = () => {
const {user, setUser} = React.useContext(UserContext);
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}

In your Navbar.js you use your loginHook hook which will create a new separate state that is different from the state used in your App.js. You need to write your hook so that is uses the context instead of useState:
/* UserContext.js */
const UserContext = createContext();
export const UserProvider = ({children}) => {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{user, setUser}}>
{children}
</UserContext.Provider>
);
}
export const useLogin = () => useContext(UserContext);
Then use it like that:
/* App.js */
import {UserProvider} from './UserContext';
const App = () => (
<UserProvider>
<Navbar />
<Content />
</UserProvider>
);
and
/* Navbar.js */
import {useLogin} from './UserContext';
const Navbar = () => {
const {user, setUser} = useLogin();
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}

Related

Passing components as state in React (Tab functionality)

Is it possible to pass other components through a state? I'm trying to make a tab function like a web browser, and if the user clicks the tab, a component shows up.
In my app.js I have -
const[chosenTab, setChosenTab] = useState("")
return (
<>
<Main chosenTab = {chosenTab}/>
</>
);
In Main.js -
const Main = ({chosenTab}) => {
return (
<>
{chosenTab}
</>
)
}
With the code below, the logic works to display the name of the tab/other component, but doesn't work if I replace {chosenTab} with <{chosenTab}/> to pass it as a component rather than just html.
I don't think this would work as you've structured it - I'd be welcome to have someone prove me wrong though since that would be a neat trick.
Now if I had to solve this problem, I'd simply use a object to hold what I need:
const tabMap = {
"string1": <component1 />,
"string2": <component2 />,
"string3": <component3 />
}
const Main = ({chosenTab}) => {
return (
<>
{tabMap[chosenTab]}
</>
)
}
Even further, let's say you wanted to pass in custom props, you could make tabMap a function to do that.
You can pass component reference itself as a tab.
const TabA = () => <div>Tab A</div>
const TabB = () => <div>Tab B</div>
const Main = ({ ChosenTab }) => {
retur <ChosenTab />
}
const App = () => {
const [chosenTab, setChosenTab] = useState(() => TabA);
const changeTab = (tab) => setChosenTab(() => tab);
return <Main ChosenTab={chosenTab} />
}
export default App;
Or you can store your tabs in object, Map or Array and set state accordingly
const tabs = {
A: TabA,
B: TabB
}
const App = () => {
const [chosenTab, setChosenTab] = useState(() => tabs.A);
const changeTab = (tabKey) => setChosenTab(() => tabs[tabKey]);
return <Main ChosenTab={chosenTab} />
}
export default App;

Why does a custom hook that uses another hooks value via useContext only shows its initial value?

I'm trying to reuse a bunch of custom hooks without re-invoking them and without maintaining an order through which I'll have to pass cascading parameters from one hook to the other.
A working example: https://codesandbox.io/s/laughing-firefly-mlhdw?file=/src/App.js:0-1158
Given the following code:
import React, { useContext, useEffect, useState } from "react";
const globalContext = React.createContext({
user: null,
pet: null
});
const usePet = () => {
const [pet, setPet] = useState(null);
useEffect(() => {
setTimeout(() => {
setPet("Dog");
}, 3000);
}, []);
return pet;
};
const useUser = () => {
const [user, setUser] = useState(null);
// I want to proxy pet via the context so that I won't have to re-invoke its side-effects again
const { pet } = useContext(globalContext);
useEffect(() => {
setTimeout(() => {
setUser("john");
}, 500);
}, []);
// This is only called once with the default value (null)
useEffect(() => {
console.log("Called from user!", { pet });
}, [pet]);
return user;
};
export const StateProvider = ({ children }) => {
const user = useUser();
const pet = usePet();
useEffect(() => {
console.log("StateProvider", { user, pet });
}, [user, pet]);
return (
<globalContext.Provider value={{ user, pet }}>
{children}
</globalContext.Provider>
);
};
export default function App() {
const { user, pet } = useContext(globalContext);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>
{pet} {user}
</h2>
</div>
);
}
// imagine an index.js that's wrapping the App component like this:
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<StateProvider>
<App />
</StateProvider>
</StrictMode>,
rootElement
);
What I'm expecting to see in the console output is the following
Called from user! {pet: null}
StateProvider {user: null, pet: null}
StateProvider {user: "john", pet: null}
StateProvider {user: "john", pet: "Dog"}
Called from user! {pet: "Dog"}
But I'm not getting any updates inside useUser other than the initial state:
Called from user! {pet: null}
StateProvider {user: null, pet: null}
StateProvider {user: "john", pet: null}
StateProvider {user: "john", pet: "Dog"}
<!-- no update here, pet is still null for the useUser hook -->
My questions are:
Is it possible to achieve that? if it is, what am I missing here?
If it's not possible, is there a more elegant way of passing data between custom hooks without re-invoking them (creating a new state context for each invocation) and without passing parameters from one another, which will force me to also maintain order between everything?
Just to clarify - the UI is working as expected and all the values are rendered correctly inside the component.
Also, when passing the parameters directly to the hook, things are also in order
const pet = usePet();
const user = useUser(pet); //this will work as it doesn't go through the context
To understand why your expectations are being violated, you must first understand how the context API works, including the useContext hook.
I'll include an especially relevant snippet here:
useContext
const value = useContext(MyContext);
Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest <MyContext.Provider> above the calling component in the tree.
When you use the context hook, your component which invokes the hook only receives updates when the context is updated. If you invoke the hook outside the root of a context provider tree, there will never be an update. This is what's happening in your example code.
An easy solution to this is to simply move the invocation of the hooks which depend on the context into a separate component below the context provider root. However, because of the way that your custom hooks are co-dependent (and neither actually update the context itself), this still leaves you in a stalemate of circular dependency.
To address this, you must give yourself the ability to update the context: by including a state setter that you can invoke with values to update the state.
In the following refactor of your code, I've made both of these changes:
This is a very common pattern, and the most common iteration of it is using the useReducer hook in combination with the context API. You can find lots of examples by querying for react state context.
<div id="root"></div><script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.17.2/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {
createContext,
useContext,
useEffect,
useState,
} = React;
const initialContextState = {pet: null, user: null};
// You don't need to supply a default value here because you're no longer
// using it outside the provider root:
// const defaultContextValue = [initialContextState, () => {}];
// const appContext = createContext(defaultContextValue);
const appContext = createContext();
const usePet = () => {
const [{pet}, setState] = useContext(appContext);
useEffect(() => setTimeout(() => setState(state => ({...state, pet: 'Dog'})), 3000), [setState]);
return pet;
};
const useUser = () => {
const [{pet, user}, setState] = useContext(appContext);
useEffect(() => setTimeout(() => setState(state => ({...state, user: 'John'})), 500), [setState]);
useEffect(() => console.log('Called from user!', {pet}), [pet]);
return user;
};
// This must be rendered within the context provider root
const ContextDependentHooksInvoker = () => {
const pet = usePet();
const user = useUser();
useEffect(
() => console.log('ContextDependentHooksInvoker', {pet, user}),
[user, pet],
);
return null;
};
const StateProvider = ({children}) => {
const stateWithSetter = useState(initialContextState);
return (
<appContext.Provider value={stateWithSetter}>
<ContextDependentHooksInvoker />
{children}
</appContext.Provider>
);
};
function App () {
const [{pet, user}] = useContext(appContext);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>{pet} {user}</h2>
</div>
);
}
function Example () {
return (
<React.StrictMode>
<StateProvider>
<App />
</StateProvider>
</React.StrictMode>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
To answer your question:
1. Yes, this is possible to achieve.
The thing that was missing here is that your useUser() is called inside StateProvider component, i.e. on the same level with the context provider, whereas for useContext() to work, it has to be called one level down the context provider (StateProvider as a wrapper). In this case, it would be your App component.
A working code would be as follow:
export default function App() {
const { user, pet } = useContext(globalContext);
// this one will print out in console
// Called from user! {pet: "Dog"}
// as expected
const userWithInContext = useUser();
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>
{pet} {user}
</h2>
</div>
);
}
I tested it and it worked. I did not make any other changes, merely setting a new userWithInContext variable up there, using the same useUser() logic you provided. In console it will print these outputs:
// this is from userWithInContext inside App component
Called from user! {pet: "Dog"}
// this is from user variable inside StateProvider component
Called from user! {pet: null}
2. Since it is possible to achieve what you want, this is just a side note about elegance and readability as of why you may not want to call useContext() inside a custom hook.

React hooks: Dynamically mapped component children and state independent from parent

I am gathering posts (called latestFeed) from my backend with an API call. These posts are all mapped to components and have comments. The comments need to be opened and closed independently of each other. I'm governing this mechanic by assigning a piece of state called showComment to each comment. showComment is generated at the parent level as dictated by the Rules of Hooks.
Here is the parent component.
import React, { useState, useEffect } from "react";
import { getLatestFeed } from "../services/axios";
import Child from "./Child";
const Parent= () => {
const [latestFeed, setLatestFeed] = useState("loading");
const [showComment, setShowComment] = useState(false);
useEffect(async () => {
const newLatestFeed = await getLatestFeed(page);
setLatestFeed(newLatestFeed);
}, []);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
return (
<div className="dashboardWrapper">
<Child posts={latestFeed} showComment={showComment} handleComment={handleComment} />
</div>
);
};
export default Parent;
latestFeed is constructed along with showComment. After latestFeed comes back with an array of posts in the useEffect hook, it is passed to the child show here:
import React, { useState } from "react";
const RenderText = ({ post, showComment, handleComment }) => {
return (
<div key={post._id} className="postWrapper">
<p>{post.title}</p>
<p>{post.body}</p>
<Comments id={post._id} showComment={showComment} handleComment={() => handleComment(post)} />
</div>
);
};
const Child = ({ posts, showComment, handleComment }) => {
return (
<div>
{posts.map((post) => {
<RenderPosts posts={posts} showComment={showComment} handleComment={handleComment} />;
})}
</div>
);
};
export default Child;
However, whenever I trigger handleComments, all comments open for all posts. I'd like them to be only the comment that was clicked.
Thanks!
You're attempting to use a single state where you claim you want multiple independent states. Define the state directly where you need it.
In order to do that, remove
const [showComment, setShowComment] = useState(false);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
from Parent, remove the showComment and handleComment props from Child and RenderText, then add
const [showComment, handleComment] = useReducer(state => !state, false);
to RenderText.

Using React Hooks in in an IIFE breaks rules of hooks

Background
I'm working on a sidebar component that can be opened from many buttons in the UI. I want to 1) only render it once, 2) grant access to update its isVisible state to these buttons without drilling props down through a common ancestor.
Expectations vs Reality
I'd expect that I could create a context with its own api method to update an internal state. In my code sample I'm attempting to do this with an IIFE.
Questions
How does this break the rules of hooks?
How else can I provide an update function to this context?
export const SidebarContext = createContext((() => {
const [isVisible, setIsVisible] = useState(false)
return {
isVisible,
toggleVisibility: () => setIsVisible(!isVisible)
}
})())
createContext() receives the default value. So you're defining a function which is invoked immediately and the result of it will be used as default value for the context. That's where the useState breaks this rule:
Call Hooks from React function components.
In order to accomplish what you want you can do this:
import React, { createContext, useContext, useState } from "react";
const SidebarContext = createContext();
function Provider({ children }) {
let [isVisible, setIsVisible] = useState(false);
let toggle = useCallback(() => setIsVisible(s => !s), [setIsVisible])
// Pass the `state` and `functions` to the context value
return (
<SidebarContext.Provider value={{ isVisible, toggle }}>
{children}
</SidebarContext.Provider>
);
}
function YourButton() {
let { isVisible, toggle } = useContext(SidebarContext);
return (
<div>
<div>Sidebar is {isVisible : 'open': 'close'}</div>
<button onClick={toggle}>
Toggle
</button>
</div>
);
}
function App() {
return (
<Provider>
<YourButton />
</Provider>
);
}

set state is not updating state

I am trying to use the state hook in my react app.
But setTodos below seems not updating the todos
link to my work: https://kutt.it/oE2jPJ
link to github: https://github.com/who-know-cg/Todo-react
import React, { useState } from "react";
import Main from "./component/Main";
const Application = () => {
const [todos, setTodos] = useState([]);
// add todo to state(todos)
const addTodos = message => {
const newTodos = todos.concat(message);
setTodos(newTodos);
};
return (
<>
<Main
addTodos={message => addTodos(message)}
/>
</>
);
};
export default Application;
And in my main.js
const Main = props => {
const input = createRef();
return (
<>
<input type="text" ref={input} />
<button
onClick={() => {
props.addTodo(input.current.value);
input.current.value = "";
}}
>
Add message to state
</button>
</>
);
};
I expect that every time I press the button, The setTodos() and getTodos() will be executed, and the message will be added to the todos array.
But it turns out the state is not changed. (still, stay in the default blank array)
If you want to update state of the parent component, you should pass down the function from the parent to child component.
Here is very simple example, how to update state with hook from child (Main) component.
With the help of a button from child component you update state of the parent (Application) component.
const Application = () => {
const [todos, setTodos] = useState([]);
const addTodo = message => {
let todosUpdated = [...todos, message];
setTodos(todosUpdated);
};
return (
<>
<Main addTodo={addTodo} />
<pre>{JSON.stringify(todos, null, 2)}</pre>
</>
);
};
const Main = props => {
const input = createRef();
return (
<>
<input type="text" ref={input} />
<button
onClick={() => {
props.addTodo(input.current.value);
input.current.value = "";
}}
>
Add message to state
</button>
</>
);
};
Demo is here: https://codesandbox.io/s/silent-cache-9y7dl
In Application.jsx :
You can pass just a reference to addTodos here. The name on the left can be whatever you want.
<Main addTodos={addTodos} />
In Main.jsx :
Since getTodo returns a Promise, whatever that promise resolves to will be your expected message.
You don't need to pass message as a parameter in Main, just the name of the function.
<Main addTodos={addTodos} />
You are passing addTodos as prop.
<Main
addTodos={message => addTodos(message)}
/>
However, in child component, you are accessing using
props.addTodo(input.current.value);
It should be addTodos.
props.addTodos(input.current.value);

Categories

Resources