I'm using ContextAPI in a small React project, I use HttpOnly Cookie to store the user's token when I hit the /login endpoint.
This is UserContext.js shown bellow, which encapsulates all the components (children) in App.js
import axios from "axios";
import { createContext, useEffect, useState } from "react";
const UserContext = createContext();
const UserContextProvider = ({ children }) => {
const [loggedUser, setLoggedUser] = useState(undefined);
const checkLoggedIn = async () => {
const response = await axios.get(`${process.env.REACT_APP_URL}/logged-in`);
setLoggedUser(response.data);
};
useEffect(() => {
checkLoggedIn();
}, []);
return (
<UserContext.Provider value={{ loggedUser }}>
{children}
</UserContext.Provider>
);
};
export { UserContext };
export default UserContextProvider;
What I understand is when I log in, I setLoggedUser to the state from the /login response, and now it is available for all the children components of the context.
Now I can navigate to all components wrapped by the context and print for example the email of the loggedUser, but what if the email changed while we're logged in? I'll still see the old email on my components because the data is outdated in the state. And what if token got invalidated on the server while we were logged in.. (The only case we'll get updated data is if I refresh the app because that will trigger useEffect in the context provider and refresh the state again)
Should I also pass the checkLoggedIn function through the context's value property to make it available for other components and then use it in UseEffect in every component? Or is there a better solution for this problem?
After the latest comment if you want to check for email on every re-render then you can remove [] from useEffect as stated above in the comments by #abu dujana.
import axios from "axios";
import { createContext, useEffect, useState } from "react";
const UserContext = createContext();
const UserContextProvider = ({ children }) => {
const [loggedUser, setLoggedUser] = useState(undefined);
const checkLoggedIn = async () => {
const response = await axios.get(`${process.env.REACT_APP_URL}/logged-in`);
setLoggedUser(response.data);
};
useEffect(() => {
checkLoggedIn();
});
return (
<UserContext.Provider value={{ loggedUser }}>
{children}
</UserContext.Provider>
);
};
export { UserContext };
export default UserContextProvider;
Related
in my Next.js app, I have a react hook that fetches the currently authenticated user, and sets it to a piece of global state. I want to run this hook once on page load, but I want it to be exposed to every component in the app
import { useState, useEffect } from 'react';
import { useQuery } from '#apollo/client';
import { GET_AUTHED_USER } from '../utils/queries';
import { useAppContext } from '../context';
export const getCurrentUser = () => {
const [isCompleted, setIsCompleted] = useState(false)
const [state, setState] = useAppContext()
const { data: authedUserData } = useQuery(GET_AUTHED_USER, {
onCompleted: () => setIsCompleted(true)
});
useEffect(() => {
Router.push('/home')
if (isCompleted) {
setState({
currentUser: authedUserData?.getAuthedUser,
isAuthed: true,
});
}
}, [isCompleted]);
return [state, setState];
_APP.js
import '../styles/reset.css'
import { AppWrapper } from '../context'
import { getCurrentUser } from '../hooks';
function MyApp({ Component, pageProps }) {
const [state] = getCurrentUser()
console.log(state) // TypeError: Invalid attempt to destructure non-iterable instance.
return (
<AppWrapper>
<Component {...pageProps} />
</AppWrapper>
)
}
export default MyApp
the hook does work in pages/index.js but that means I can only run it if the / endpoint is hit.
<AppWrapper/> is where all the values get originally defined
import { createContext, useContext, useState } from 'react';
import { ApolloClient, InMemoryCache, ApolloProvider, createHttpLink } from '#apollo/client';
import { setContext } from '#apollo/client/link/context';
import { getCookie } from '../utils/functions';
const AppContext = createContext();
export function AppWrapper({ children }) {
const URI = 'http://localhost:5000/graphql';
const [state, setState] = useState({
currentUser: null,
isAuthed: false,
});
const httpLink = createHttpLink({
uri: URI,
});
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = getCookie('JWT');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? token : '',
}
}
});
const client = new ApolloClient({
cache: new InMemoryCache(),
link: authLink.concat(httpLink)
});
return (
<AppContext.Provider value={[state, setState]}>
<ApolloProvider client={client}>
{children}
</ApolloProvider>
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
Interesting question, you want to load that portion of code only once per each browser hit?
Then the location is right. NextJs make sure when you have a unique browser hit, it runs _app.js, but only once, after that it'll goes into a single page application mode.
After the above fact, actually whether a piece of code is run only once or twice or multiple time is mostly driven by how many times it detects the "change".
useEffect(() => {
// run
}, [condition])
If the condition changes, it'll run again. However if the condition does not change, but the whole piece is re-mount, it'll run again. You have to consider both fact here.
In short, if you have to run it per route change, make the condition === route.name. A piece of advice, try work with the single page application first, then work with the unique feature nextJS, because otherwise it'll be really difficult to figure out the answer.
I am initializing a TS React App connected to Firebase with a private route when the user is logged in.
Everything seems to work well until I refresh the page. When I do that, the app takes me back to the public route which is a login page.
I think that the problem could be the initial state of the context which is set to null, but maybe it's a different problem.
Here is the code for my user context:
import React, { useContext, useState, useEffect } from "react";
import firebase from "firebase/app";
import { auth } from "../firebase";
export const AuthContext = React.createContext<firebase.User | null>(null);
export function useAuth() {
return useContext(AuthContext);
}
export const AuthProvider: React.FC = ({ children }) => {
const [user, setUser] = useState<firebase.User | null>(null);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((firebaseUser) => {
setUser(firebaseUser);
});
return unsubscribe;
}, []);
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
Here is how I created the private route:
import React, { useContext } from "react";
import { Redirect, Route, RouteProps } from "react-router";
import { AuthContext } from "../contexts/AuthContext";
interface IPrivateRoute extends RouteProps {
component: any;
}
const PrivateRoute = ({ component: Component, ...rest }: IPrivateRoute) => {
const user = useContext(AuthContext);
setTimeout(() => console.log(user), 1000);
return (
<div>
<Route
{...rest}
render={(props) => {
return user ? <Component {...props} /> : <Redirect to="/login" />;
}}>
</Route>
</div>
);
};
export default PrivateRoute;
I will be grateful for all the helpful answers!
I think you need to save the session to stay authenticated. Whenever you refresh the page your user state will be null.
What worked for me is I used JWT. https://jwt.io/
After user logs in successfully my server sends the user a token and I save the token in user's cookies. For each PrivateRoute the user requests they will send the token back to server for verification. If the verification is successful then return the PrivateRoute to them.
I just started playing with context today and this is my usercontext
import { createContext, useEffect, useState } from "react";
import axios from "axios";
export const userContext = createContext({});
const UserContext = ({ children }) => {
const [user, setUser] = useState({});
useEffect(() => {
axios.get("/api/auth/user", { withCredentials: true }).then((res) => {
console.log(res);
setUser(res.data.user);
});
}, []);
return <userContext.Provider value={user}>{children}</userContext.Provider>;
};
export default UserContext;
this is how im using it in any component that needs the currently logged in user
const user = useContext(userContext)
my question is whenever the user logs in or logs out I have to refresh the page in order to see the change in the browser. is there any way that I can do this where there does not need to be a reload. also any general tips on react context are appreciated
(EDIT)
this is how Im using the UserContext if it helps at all
const App = () => {
return (
<BrowserRouter>
<UserContext>
<Switch>
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
component={route.component}
/>
))}
</Switch>
</UserContext>
</BrowserRouter>
);
};
Where is your context consumer?
The way it is set up, any userContext.Consumer which has a UserContext as its ancestor will re render when the associated user is loaded, without the page needing to be reloaded.
To make it clearer you should rename your UserContext component to UserProvider and create a corresponding UserConsumer component:
import { createContext, useEffect, useState } from "react";
import axios from "axios";
export const userContext = createContext({});
const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
useEffect(() => {
axios.get("/api/auth/user", { withCredentials: true }).then((res) => {
console.log(res);
// setting the state here will trigger a re render of this component
setUser(res.data.user);
});
}, []);
return <userContext.Provider value={user}>{children}</userContext.Provider>;
};
const UserConsumer = ({ children }) => {
return (
<userContext.Consumer>
{context => {
if (context === undefined) {
throw new Error('UserConsumer must be used within a UserProvider ')
}
// children is assumed to be a function, it must be used
// this way: context => render something with context (user)
return children(context)
}}
</userContext.Consumer>
);
};
export { UserProvider, UserConsumer };
Usage example:
import { UserConsumer } from 'the-file-containing-the-code-above';
export const SomeUiNeedingUserInfo = props => (
<UserConsumer>
{user => (
<ul>
<li>{user.firstName}</>
<li>{user.lastName}</>
</ul>
)}
</UserConsumer>
)
To be fair, you could also register to the context yourself, this way for a functional component:
const AnotherConsumer = props => {
const user = useContext(userContext);
return (....);
}
And this way for a class component:
class AnotherConsumer extends React.Component {
static contextType = userContext;
render() {
const user = this.context;
return (.....);
}
}
The benefit of the UserConsumer is reuasability without having to worry if you're in a functional or class component: it will used the same way.
Either way you have to "tell" react which component registers (should listen to) the userContext to have it refreshed on context change.
That's the whole point of context: allow for a small portion of the render tree to be affected and avoid prop drilling.
I am pretty new to Next.JS and I was trying to set up Redux with my Next.JS application. Now my page is supposed to display a list of posts that I am calling in from an API. The page renders perfectly when I'm dispatching from useEffect() to populate the data on to my page, but getStaticProps() or getServerSideProps() are not working whatsoever!
Here is a bit of code that will give you a hint of what I've done so far:
store.js
import { useMemo } from 'react'
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
import rootReducer from './reducers/rootReducer'
const initialState = {}
const middlewares = [thunk]
let store
function initStore(preloadedState = initialState) {
return createStore(
rootReducer,
preloadedState,
composeWithDevTools(applyMiddleware(...middlewares))
)
}
export const initializeStore = (preloadedState) => {
let _store = store ?? initStore(preloadedState)
if (preloadedState && store) {
_store = initStore({
...store.getState(),
...preloadedState,
})
store = undefined
}
if (typeof window === 'undefined') return _store
if (!store) store = _store
return _store
}
export function useStore(initialState) {
const store = useMemo(() => initializeStore(initialState), [initialState])
return store
}
action.js
export const fetchPosts = () => async dispatch => {
const res = await axios.get('https://jsonplaceholder.typicode.com/posts')
dispatch({
type: FETCH_POSTS,
payload: res.data
})
}
_app.js
import { Provider } from 'react-redux'
import { createWrapper } from 'next-redux-wrapper'
import { useStore } from '../redux/store'
export default function MyApp({ Component, pageProps }) {
const store = useStore(pageProps.initialReduxState)
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
}
These are the files that I needed for the basic redux setup. Once my store was set up and I wrapped my app around the Provider, I initially though of using useEffect() hook to populate data on a component that was rendering inside my index.js file.
component.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchPosts } from '../redux/actions/postsAction'
const Posts = () => {
const dispatch = useDispatch()
const { items } = useSelector(state => state.posts)
useEffect(() => {
dispatch(fetchPosts())
}, [])
return (
<div className="">
<h1>Posts</h1>
{items.map(post => {
return (<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>)
})}
</div>
)
}
export default Posts
This worked perfectly! All my posts were showing up inside the component. The problem occurred when I was trying to achieve the same behaviour with server side rendering (or even SSG). I wanted to populate the data during the pre-render phase but for some reason the items array which is supposed to hold all the data is empty, basically meaning that the disptacher was never called! Here is the piece of code that is bothering me (exactly same as previous code, but this time I'm using getStaticProps() instead of useEffect()):
component.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchPosts } from '../redux/actions/postsAction'
const Posts = ({ items }) => {
return (
<div className="">
<h1>Posts</h1>
{items.map(post => {
return (<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>)
})}
</div>
)
}
export async function getStaticProps() {
console.log('Props called')
const dispatch = useDispatch()
const { items } = useSelector(state => state.posts)
dispatch(fetchPosts())
console.log(items)
return { props: { items } }
}
export default Posts
By running this, I'm getting an error that items is empty! Please help me, I have no clue what's going wrong here.
Well I fixed this issue myself but I forgot to post an answer for it, my bad!
The problem here really is very simple, hooks don't work outside of a functional component!
I think, inside of getStaticProps just call API or get datas from DB and returns it as props to pages/index.js (any component you want) and inside of this component we can get datas from getStaticProps as props.
Also we can set it as global state using useDispatch of react-redux. After that any component we can call those states using redux mapStateToProps. This is my solution.
This maybe a solution if anyone faced this problem,
import React from 'react';
import {useSelector} from 'react-redux';
import {wrapper} from '../store';
export const getStaticProps = wrapper.getStaticProps(store => ({preview})
=> {
console.log('2. Page.getStaticProps uses the store to dispatch things');
store.dispatch({
type: 'TICK',
payload: 'was set in other page ' + preview,
});
});
// you can also use `connect()` instead of hooks
const Page = () => {
const {tick} = useSelector(state => state);
return <div>{tick}</div>;
};
export default Page;
Got it from here: https://github.com/kirill-konshin/next-redux-wrapper
I have come across a problem implementing a firebase-auth context in my app. All seemed well until I tried to handle persistence with firebase. As you can see below, firebase uses an observable called onAuthStateChanged to grab the logged in user, and this can be used to update an auth object to pass down via a provider. When you refresh that app, the observable will eventually return an auth object again, which should update the provider, and update all the components that consume the auth context in the react tree.
At first I get an error: TypeError: Cannot read property 'user' of undefined
If I log state within my consuming component, I get: Auth state within component undefined
This is my component that acts as a provider to the rest of the app component tree.
AuthContext.js
import React from "react";
import * as firebase from "firebase/app";
import { firebaseAuth } from "../reducers/AuthReducer";
export const Auth = React.createContext();
export const AuthProvider = (props) => {
const [state, dispatch] = React.useReducer(
firebaseAuth,
{ user: {} },
firebase.auth().onAuthStateChanged((user) => {
return user;
})
);
return (
<Auth.Provider value={{ state, dispatch }}>{props.children}</Auth.Provider>
);
};
This is my component that consumes the provided auth context:
RightPanel.js
import React from "react";
import HomePageAuth from "./homepageauth/HomePageAuth";
import "./rightpanel.module.less";
import NavCluster from "../../../nav/navcluster/NavCluster";
import { Auth } from "../../../../contexts/AuthContext";
const RightPanel = () => {
const { state, dispatch } = React.useContext(Auth);
console.log("Auth state within component", state);
return (
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "80px",
width: "50%",
}}
>
{state.user ? <NavCluster /> : <HomePageAuth />}
</div>
);
};
export default RightPanel;
In your init you are returning user, not an object with user as a key. See https://reactjs.org/docs/hooks-reference.html#lazy-initialization
firebase.auth().onAuthStateChanged((user) => {
return {user};
})