Reputation: 311
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.
Upvotes: 5
Views: 4186
Reputation: 517
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
Upvotes: 4
Reputation: 281686
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>
}
Upvotes: 0
Reputation: 16309
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>
}
Upvotes: -1
Reputation: 1074266
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
).
Upvotes: 0