Nextjs: How to use ContextAPI with multiple values all of which need to be updated from child components - javascript

I store three values in the context provider wrapper function. I need to update the context states in the login and logout components, and use them in navbar and in any other places.
const AppContext = createContext();
const AppContextProvider = (props) => {
const loggedIn = Cookies.get('isLoggedIn') ? true : false;
const [ isLoggedIn, setIsLoggedIn ] = useState(isLoggedIn);
const [ userID, setUserID ] = useState('');
const [ email, setEmail ] = useState('');
return (
<AppContext.Provider value={[isLoggedIn, setIsLoggedIn, userID, setUserID, email, setEmail ]}>
{props.children}
</AppContext.Provider>
);
}
export { AppContext, AppContextProvider };
In the _app.js it's this:
function MyApp({ Component, pageProps }) {
return (
<>
<AppContextProvider>
<NavBar />
<Component {...pageProps} />
<Footer />
</AppContextProvider>
</>
);
}
But now, the trouble is, I can't understand how to update these context states. This is my use case in login component:
const [ isLoggedIn, setIsLoggedIn, userID, setUserID, email, setEmail ] = useContext(AppContext);
const handleSubmit = async() => {
const url = process.env.NEXT_PUBLIC_URL + 'auth/login/';
const data = { "email": email, "password": password };
try {
const resp = await axios.post(url, data);
const obj = await resp.data;
Cookies.set('isLoggedIn', true, { secure: true }, { sameSite: 'lax' });
setIsLoggedIn(true);
setUserID(obj.uid);
setEmail(obj.email);
} catch (err) {...};
This way, it didn't work. I could not get the context values in other components. How do I update multiple context states?
EDIT:
Following #Drew Reese's answer, this is my working Context file:
import Cookies from 'js-cookie';
import { useState, createContext } from 'react';
const AppContext = createContext({
email:'',
isLoggedIn: false,
userID: '',
setEmail: () => {},
setIsLoggedIn: () => {},
setUserID: () => {},
});
const AppContextProvider = (props) => {
const [ email, setEmail ] = useState(Cookies.get('email') || null);
const [ userID, setUserID ] = useState(Cookies.get('uid') || null);
const [ isLoggedIn, setIsLoggedIn ] = useState(Cookies.get('isLoggedIn') ? true : false);
return (
<AppContext.Provider value={{ email, setEmail, isLoggedIn, setIsLoggedIn, userID, setUserID }}>
{props.children}
</AppContext.Provider>
);
}
export { AppContext, AppContextProvider };

Your default context value should match what consumers expect.
Updating Context from a Nested Component
It is often necessary to update the context from a component that is
nested somewhere deeply in the component tree. In this case you can
pass a function down through the context to allow consumers to update
the context:
// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
Update your AppContext default value to match what consumers will be using.
const AppContext = createContext([
false, // isLoggedIn
() => {}, // setIsLoggedIn
'', // userID
() => {}, // setUserID
'', // email
() => {}, // setEmail
]);
Using an array like this may be a little cumbersome, consumers would need to keep the array indices straight when using destructuring assignment. Using an object instead makes consuming the context value a little more wieldy, now the order is irrelevant.
const AppContext = createContext({
email: '',
isLoggedIn: false,
userID: '',
setEmail: () => {},
setIsLoggedIn: () => {},
setUserID: () => {},
});
Usage:
const {
isLoggedIn,
setIsLoggedIn,
userID,
setUserID,
email,
setEmail,
} = useContext(AppContext);
I could not get the context values in other components. How do I
update multiple context states?
You should ensure that all consumers that you want to be able to update the context value are actually nested in the same AppContextProvider component.

Related

User is returning null even after modifying it with context API

I am making an API call using Axios and after that I am send those details to context API but I am getting null. I am using formik to send data to backend and on submit of that form I make an api call using axios then get the user from backend end pass it on to context API.
UserContext
import { createContext, useReducer } from "react";
import UserReducer from "./UserReducer";
const INITIAL_STATE = {
user: null,
};
export const UserContext = createContext(INITIAL_STATE);
export const UserContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(UserReducer, INITIAL_STATE);
const setUser = (userDetails) => {
dispatch({
type: "GET_USER",
payload: userDetails,
});
// Here it is returning the user data but INITIAL_STATE.user is null
};
return (
<UserContext.Provider
value={{
user: state.user,
setUser,
}}
>
{children}
</UserContext.Provider>
);
};
UserReducer
const UserReducer = (state, action) => {
switch (action.type) {
case "GET_USER":
return {
user: action.payload,
};
default:
return state;
}
};
export default UserReducer;
Login
const { user, setUser } = useContext(UserContext);
const formik = useFormik({
initialValues: {
email: "",
password: "",
},
onSubmit: () => {
const getUser = async () => {
const userData = await Axios.post("http://localhost:3001/login", {
email: formik.values.email,
password: formik.values.password,
});
setUser(userData.data); // Here I am sending the data to context API
};
getUser();
},
validationSchema,
});

React.useState, will not update. How do I perform a instant update of React useState?

So I'm setting up a react useContext to share user's data such as the user's name and the login status etc... I make a request to the server every time a user refreshes the app to verify that if they have a valid token, the user can then be logged in. Great, but one problem I keep on running into with react is knowing how to get an instant update of useState, as this is needing to be updated as soon as the user is verified.
I've read that you have to create a new object so that React knows it's a new object and then it'll trigger a re-render... Just about every combination I can think of I've tried. Can someone please clarify for me how it's done?
Option 1:
setUser(prev => {
return { ...prev, ...newObj }
})
Option 2:
setUser({ ...newObj })
Can someone please give me the proper way to get a instant update to the React useState.
Thanks
import React, { useState } from "react";
import { useReadCookie } from "../hooks/cookies";
import UserContext from "./user-context";
const UserContextProvider = ({ children }) => {
const readCookie = useReadCookie;
const [user, setUser] = useState({
name: null,
userID: null,
token: null,
permissions: null,
loginStatus: false,
checkLoginStatusReady: false,
});
const loginStatus = async () => {
return new Promise(async (resolve, rej) => {
// So we'll reach out to the server and check to see if the
// User is logged in
const token = readCookie("token");
if (token && token.length === 174) {
// send a request to see if this is a valid user
const req = await fetch("/api/checkLoginStatus.php");
const res = await req.json();
// console.log(res);
handleLogUserIn(res);
}
console.log(user, "The state variable");
resolve();
});
};
const handleLogUserIn = (res) => {
if (res.success === true) {
const userObj = {
name: res.name,
userID: res.userID,
token: res.token,
permissions: res.permissions,
loginStatus: true,
checkLoginStatusReady: true,
};
console.log(userObj, "the user variable");
setUser({ ...userObj });
}
else {
console.log("Not going here");
const userObj = {
name: null,
userID: null,
token: null,
permissions: null,
loginStatus: false,
checkLoginStatusReady: true,
};
setUser({ ...userObj });
}
};
return (
<UserContext.Provider
value={{
username: user.username,
userID: user.userID,
token: user.token,
permissions: user.permissions,
loginStatus: user.loginStatus,
checkLoginStatusReady: loginStatus,
setUser,
}}
>
{children}
</UserContext.Provider>
);
};
export default UserContextProvider;
you can use useEffect for listening to the state update
example if using
const [state, setState] = useState({
username : "",
status : "",
})
useEffect(() => {
setState({
...state,
username : state.username,
status : "active,
})
},[state])
or if using context :
On parent provider.js:
import React, { useState } from 'react'
export const UserContext = React.createContext({
username: "",
status: "active",
setUser: () => {}
})
export const UserContextProvider = (props) => {
const setUser = (param) => {
setState({...state, username: param.username, status: param.status})
}
const initState = {
username: "",
status: "active",
setUser: () => {}
}
const [state, setState] = useState(initState)
return (
<UserContext.Provider value={state}>
{props.children}
</UserContext.Provider>
)
}
On Component:
import React, { useContext } from 'react'
import provider from './provider'
function Component() {
const context = useContext(provider.UserContext)
const onClick = () => {
// do hit api
if(token){
state.setUser({username : "a", status:"inactive"})
}
}
return (
<provider.UserContextProvider>
<buttin onClick={onClick}></button>
<div>username : {state.username}</div>
<div>status : {state.status}</div>
</provider.UserContextProvider>
)
}

reactjs: a context component function not updating its variable; usestate not working immediately?

I'm working on auth in my app, so I created a context to manage it:
import React, { useState } from "react";
const AuthContext = React.createContext({
token: "",
isLoggedIn: false,
login: (token) => { },
logout: () => { },
});
export const AuthContextProvider = (props) => {
const [token, setToken] = useState(null);
const [userIsLoggedIn, setUserLoggedIn] = useState(false)
const loginHandler = async (token) => {
setToken(token);
setUserLoggedIn(true)
console.log(userIsLoggedIn) //prints false
};
const logoutHandler = () => {
setToken(null);
setUserLoggedIn(false)
};
const contextValue = {
token: token,
isLoggedIn: userIsLoggedIn,
login: loginHandler,
logout: logoutHandler,
};
return (
<AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
The problem is that when I call context.login('some token') from an outside component and then console.log(context.isLoggedIn) I get false the first time, but if I call context.login('some token') once again I get true. Every succesive time after that I get true now. It might have to do with the fact the "console.log(userIsLoggedIn)" inside the loginHandler says "false" (the first time the context.login is called) even though the setUserLoggedIn is right above it. I think the userIsLoggedIn variable is changed after the context is updated in the component it's used in, but I don't know how to fix that.
I will appreciate any help on this
You cannot rely on the value of userIsLoggedIn to have the value you passed to setUserLoggedIn immediately.
If you want to keep track of the userIsLoggedIn variable, you might consider something like this:
React.useEffect(() => {
console.log(userIsLoggedIn)
}, [userIsLoggedIn])

Chain query/mutation calls with RTK Query using React Hooks

In the example bellow I'm trying to create a simple login/authentication component with React and Redux Toolkit.
const Login = () => {
const dispatch = useDispatch();
const [login, { isLoading }] = useLoginMutation();
const [loginData, setLoginData] = useState({
email: '',
password: '',
});
const loginHandler = async () => {
try {
const result = await login({
email: loginData.email,
password: loginData.password,
}).unwrap();
const { token, userId } = result;
const { data } = await dispatch(
backendApi.endpoints.getUser.initiate({ token, userId })
);
dispatch(
setCredentials({
user: {
email: data.user.email,
id: data.user.id,
name: data.user.name,
type: data.user.type,
},
token,
})
);
} catch (err) {
console.log(err);
}
};
return (
<Space>
{isLoading ? (
<SpinnerIcon />
) : (
// setLoginData and loginHandler are set and used here
// Standard React code: Input onChange -> setLoginData / Submit button onClick -> loginHandler
// (nothing special, code shortened for the sake of readability)
<FormWithEmailPasswordInputsAndButton />
)}
</Space>
);
};
This works fine, the problem is that:
I want to make use of the isLoading property on the getUser query result object so that the <SpinnerIcon /> component will be visible until both requests are done.
As it is, the spinnerIcon component is only shown during the login API call because I'm not using React Hooks to call getUser query therefore I don't have the isLoading property from getUser available to the rest of the component.
But if I want to use React Hooks to make the call I'll need the result from the login mutation beforehand.
const [getUser, { isLoading: isUserLoading }] = useGetUserQuery(resultFromLoginMutation);
And I don't know how to do that. One after the other, so that I can make use of both isLoading properties.
You can use skipToken or the skip option:
const [login, { isLoading, data: resultFromLoginMutation, isSuccess }] = useLoginMutation();
const [getUser, { isLoading: isUserLoading }] = useGetUserQuery(resultFromLoginMutation, { skip: !isSuccess });
or
const [login, { isLoading, data: resultFromLoginMutation }] = useLoginMutation();
const [getUser, { isLoading: isUserLoading }] = useGetUserQuery(resultFromLoginMutation ?? skipToken);

Redux state not updating after action dispatched

I have a form for users to enter their details and press submit. This is supposed to dispatch an action and update the state by .concat() a class to it. Unfortunately the state isn't updating and I don't know why. If I take out useCallBack() or useEffect() from the code , the emulator freezes and I suspect infinite loops.
Redux Reducer
// Initialised class
import newAccount from '../../models/newAccount'
import { CREATE_ACCOUNT } from '../actions/meals'
const initialState = {
account: [],
}
const addPerson = (state=initialState, action) =>{
switch(action.type){
case CREATE_ACCOUNT:
const newAccount = new newAccount(
Date.now().toString(),
action.accountData.name,
action.accountData.image,
action.accountData.email,
action.accountData.password
)
return { ...state, account: state.account.concat(newAccount) }
default:
return state
}
}
export default addPerson
Redux action
export const CREATE_ACCOUNT = 'CREATE_ACCOUNT'
export const newAccount = (Id,name,image, email, password) => {
return {type: CREATE_ACCOUNT, accountData:{
Id: Date.now().toString(),
name: name,
image: image,
email: email,
password: password
}
}
}
The class
class newAccount {
constructor(
id,
name,
image,
email,
password
){
this.id = id;
this.name = name;
this.image = image;
this.email = email;
this.password = password;
}
}
export default newAccount
The Component
import React, { useState, useCallback, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {newAccount} from '../Store/actions/accounts'
import ImagePicker from '../Components/ImagePicker'
const AddScreen = (props) => {
const dispatch = useDispatch()
const [name, setName] = useState('')
const [selectedImage, setSelectedImage] = useState('')
const email = useSelector(state => state.account.email)
const password = useSelector(state => state.account.password)
const handleSubmit = useCallback(() => {
dispatch(newAccount(Date.now(),name,selectedImage,email,password))
},[dispatch, name, selectedImage, email, password])
useEffect(() => { handleSubmit
props.navigation.setParams({handleSubmit: handleSubmit})
},[handleSubmit])
return (
<View style={styles.container}>
<View style={styles.card}>
<ImagePicker onImageSelected={selectedImage} />
<AddForm email={email} password={password}/>
<TextInput
onChangeText={name => setName(name)}
value={name}
/>
</View>
</View>
)
}
export default AddScreen
AddScreen.navigationOptions = (navigationData) => {
const submit = navigationData.navigation.getParam('handleSubmit')
return {
headerTitle: 'Create Account',
headerRight: () => (
<TouchableOpacity onPress={submit}>
<Text style={styles.createOrange}>Create</Text>
</TouchableOpacity>
)
}
}
I really don't know why it's not updating .
first of all, you shouldn't store classes in the redux store, the store should only exists of plain objects. but if you really want to store the class:
The real problem seams to be return { ...state, account: state.account.concat(newAccount) }. here you concat the existing array with the new class, but that doesn't work.
your store looks like this if you do so:
{
account: [{
email: "..."
id: "..."
image: "..."
name: "..."
password: "...
}],
}
so your selector (state.account.email) will return undefined. you can use (state.account[0].email)
or you can fix it by fixing the real problem:
return { ...state, account: newAccount }
also your initialState shouldn't be a an array for account as it will never be an array, it will be an Account class (this is why you don't get an error by what you are doing). set it to null.
const initialState = {
account: null,
}
I really don't know why this doesn't work. Just want to give you an advice to make it more simple and clearer (from my point of view):
You can drop side effects like useEffect. To achieve this just move local state to redux state and then you will be able to just dispatch the action from your navigationOptions component. It could look like:
const AddScreen = () => {
const name = useSelector(...);
...
const password = useSelector(...);
// trigger action on something changes, for instance like that:
const onChange = (key, value) = dispatch(newAccountChange({[key]: value}))
// return tree of components
}
export const submitNewAccount = () => {
return (dispatch, getState) => {
const { id, name, ... } = getState().account;
dispatch(newAccount(id, name, ...));
};
}
AddScreen.navigationOptions = (navigationData) => {
const dispatch = useDispatch();
const submit = dispatch(submitNewAccount());
...
}
I used redux-thunk in this example.
I believe, this approach will give you more flexible way to debug and extend your business logic.

Categories

Resources