quick question about react context and rerendering components - javascript

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.

Related

Does the change of router parameter in React Route path cause remount of Component and other initial render respectively?

I have a question about react router. I'm using different paths for the same component. The path is different since a changeable route parameter is given inside. So, does the change of the parameter causes the component to remount/unmount? If yes, does the remount of the component cause an initial render again? (I'm using functional components. I'm building a chatapp. There will be different chatrooms and the user can navigate to them from the home component.)
import './Home.css'
import {BrowserRouter as Router, Route, Switch, useHistory} from 'react-router-dom';
import { Link } from 'react-router-dom/cjs/react-router-dom.min';
import ChatRoom from './ChatRoom';
import { useEffect, useState } from 'react';
const Home = ({db, auth, user}) => {
const history = useHistory();
const [user1, setUser1] = useState('Loading...');
const logOut = () => {
auth.signOut().then(() => {
history.replace("/");
});
}
useEffect(() => {
db.collection('users').get().then(snapshot => {
snapshot.docs.forEach(doc => {
if (doc.id === user.uid) {
setUser1(doc.data().username);
}
})
}).catch(error => {
console.log(error);
})
}, [])
return (
<Router>
<div>
<Link to="/rooms/ChatA">ChatA</Link>
<Link to="/rooms/ChatB">ChatB</Link>
<Switch>
<Route path="/rooms/:ChatID">
<ChatRoom db={db} auth={auth}/>
</Route>
<Switch>
</div>
</Router>
)
}
export default Home;
This is the component where the user will navigate to particular chatroom from.
import React, { useState, useEffect, useRef } from 'react'
import { useParams } from 'react-router-dom';
import {firebase} from '../../Firebase/firebase';
const ChatRoom = ({ db, auth }) => {
const { chatID } = useParams();
const [user, setUser] = useState(null);
const [docs, setDocs] = useState(null)
const [msg, setMsg] = useState('');
const dummy = useRef();
const sendMsg = (e) => {
e.preventDefault();
db.collection(chatID).add({
username: user,
text: msg,
createdAt: firebase.firestore.FieldValue.serverTimestamp()
}).then(() => {
dummy.current.scrollIntoView({ behavior: 'smooth' });
})
setMsg('');
}
const deleteMsg = (id) => {
db.collection(chatID).doc(id).delete();
}
useEffect(() => {
auth.onAuthStateChanged(user => {
if (user) {
db.collection('users').get().then(snapshot => {
snapshot.docs.forEach(doc => {
if (doc.id === user.uid) {
setUser(doc.data().username);
}
})
})
}
})
db.collection(chatID).orderBy('createdAt').onSnapshot(snapshot)
=> {
setDocs(snapshot.docs);}
}, [])
return (
<div>
//some contents
</div>
);
}
export default ChatRoom;
This is the component which will be mounted according to which chatroom the user will navigate to.
I'll repeat my question: Will the change of the route parameter value cause a remount of the ChatRoom component and cause another initial render respectively?

React Redux & ContextApi - How to pass a prop via context and keeping it "connected"?

Considering the following project setup on a react-redux application that uses context API to avoid prop drilling. The example given is simplified.
Project Setup
React project uses React Redux
Uses context API to avoid prop drilling in certain cases.
Redux store has a prop posts which contains list of posts
An action creator deletePost(), which deletes a certain post by post id.
To avoid prop drilling, both posts and deletePosts() is added to a context AppContext and returned by a hook funciton useApp().
posts array is passed via contexts so it is not used by connect() function. Important
Problem:
When action is dispatched store is updated however Component is not re-rendered (because the prop is not connected?). Of course, if I pass the prop with connect function and drill it down to child rendering works fine.
What is the solution?
Example Project
The example project can be found in codesandbox. Open up the console and try to click the delete button. You will see no change in the UI while you can see the state is updated in the console.
Codes
App.js
import Home from "./routes/Home";
import "./styles.css";
import { AppProvider } from "./context";
export default function App() {
return (
<AppProvider>
<div className="App">
<Home />
</div>
</AppProvider>
);
}
context.js
import { useDispatch, useStore } from "react-redux";
import { useContext, createContext } from "react";
import { deletePost } from "./redux/actions/posts";
export const AppContext = createContext();
export const useApp = () => {
return useContext(AppContext);
};
export const AppProvider = ({ children }) => {
const dispatch = useDispatch();
const {
posts: { items: posts }
} = useStore().getState();
const value = {
// props
posts,
// actions
deletePost,
dispatch
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
Home.js
import { connect } from "react-redux";
import Post from "../components/Post";
import { useApp } from "../context";
const Home = () => {
const { posts } = useApp();
return (
<section>
{posts.map((p) => (
<Post key={p.id} {...p} />
))}
</section>
);
};
/*
const mapProps = ({ posts: { items: posts } }) => {
return {
posts
};
};
*/
export default connect()(Home);
Post.js
import { useApp } from "../context";
const Post = ({ title, content, id }) => {
const { deletePost, dispatch } = useApp();
const onDeleteClick = () => {
console.log("delete it", id);
dispatch(deletePost(id));
};
return (
<article>
<h1>{title}</h1>
<p>{content}</p>
<div className="toolbar">
<button onClick={onDeleteClick}>Delete</button>
</div>
</article>
);
};
export default Post;
You're not using the connect higher order component method properly . Try using it like this so your component will get the states and the function of your redux store :
import React from 'react';
import { connect } from 'react-redux';
import { callAction } from '../redux/actions.js';
const Home = (props) => {
return (
<div> {JSON.stringify(props)} </div>
)
}
const mapState = (state) => {
name : state.name // name is in intialState
}
const mapDispatch = (dispatch) => {
callAction : () => dispatch(callAction()) // callAction is a redux action
//and should be imported in the component also
}
export default connect(mapState , mapDispatch)(Home);
You can access the states and the actions from your redux store via component props.
Use useSelector() instead of useState(). Example codepen is fixed.
Change from:
const { posts: { items: posts } } = useStore().getState();
Change to:
const posts = useSelector(state => state.posts.items);
useStore() value is only received when component is first mounted. While useSlector() will get value when value is changed.

Render route in React router dom after useEffect

I need to render the component after the useEffect checks if an user has the required role, but it always redirect me because it execute first the render function and then the useEffect
Here's my code:
import { Route, Redirect } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { checkIfUserHasRequiredRole } from '../../utility/utility';
import PathConst from '../../utils/PathConst';
const AuthRoute = ({
Component, path, exact = false, isAuthenticated, requiredRoles,
}) => {
const [userHasRequiredRole, setUserHasRequiredRole] = useState(false);
const roles = useSelector((state) => state.role.roles);
const isAuthed = isAuthenticated;
useEffect(() => {
if (roles) {
const userRole = checkIfUserHasRequiredRole(requiredRoles, roles);
setUserHasRequiredRole(userRole);
}
}, [roles]);
return (
<Route
exact={exact}
path={path}
render={() => (
isAuthed && userHasRequiredRole ? (<Component />)
: (
<Redirect
to={PathConst.toLoginPage}
/>
))}
/>
);
};
export default AuthRoute;
The function 'checkIfUserHasRequiredRole' returns true but 'useHasRequiredRole' is still false when it trigger the if statement. I also tried using the same useEffect function with no dependencies.
How can I manage it?
You could do with your setup, but that requires additional render so that userHasRequiredRole is updated and take effect.
Since you can figure out what you need based on roles, you could,
import { useHistory } from "react-router-dom";
const history = useHistory();
useEffect(() => {
if (roles) {
const userRole = checkIfUserHasRequiredRole(requiredRoles, roles);
if (this role is not good) {
history.push(PathConst.toLoginPage)
}
}
}, [roles])

Get logged user every time the component renders under React ContextAPI

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;

React: Class Based Context API to Hook Based context API

I am really new in Hooks and during learning faces many difficulties to switch from the old style.
My old code looks like:
context.js
import React from "react";
const SomeContext = React.createContext(null);
export const withContext = (Component) => (props) => (
<SomeContext.Consumer>
{(server) => <Component {...props} server={server} />}
</SomeContext.Consumer>
);
export default SomeContext;
main index.js
<SomeContext.Provider value={new SomeClass()}>
<App />
</SomeContext.Provider>
but when I want to access it through with export default withContext(SomeComponent) by this.props.server.someFunc() it showed props is undefined in the classless hook function.
how can I achieve this.props in react hook
Edit:
SomeClass is not React inheriting class and its look like it.
class SomeClass {
someFunc = (id) => axios('api endpoints')
}
SomeComponent
const SomeComponent = () => {
...
useEffect(() => {
this.props.server.someFunc(id).then(...).catch(...)
}, ...)
...
}
In the regular case, you need to export the Context, then import it and use it within useContext:
export const SomeContext = React.createContext(null);
// Usage
import { SomeContext } from '..';
const SomeComponent = () => {
const server = useContext(SomeContext);
useEffect(() => {
server.someFunc(id);
});
};
But in your case, since you using HOC, you have the server within the prop it self:
const SomeComponent = (props) => {
useEffect(() => {
props.server.someFunc(id);
});
};

Categories