Value of react context is not updated [duplicate] - reactjs

This question already has answers here:
Why does calling react setState method not mutate the state immediately?
(9 answers)
Closed 1 year ago.
I have a client implemented in react typescript, which needs to work with user data.
Therefore, I've created an AppContext.
//appState.ts
export interface UserStateProperties {
authenticated: boolean,
user: GetUserResponse | undefined,
notificationManager: NotificationManager | undefined
}
export interface AppContextProperties {
userState: UserStateProperties,
setUserState: Dispatch<SetStateAction<UserStateProperties>>
}
const AppContext = React.createContext<AppContextProperties>({
userState: {
authenticated: false,
user: undefined, // userData like name, level, ...
notificationManager: undefined // contains socket to receive notifications
},
setUserState: () => {}
});
export default AppContext;
In my App component, I instantiate a state for the user and passed it as value to an AppContext.Provider.
// App.tsx
function App() {
const [userState, setUserState] = useState<UserStateProperties>({
authenticated: false,
user: undefined,
notificationManager: undefined
});
return (
<Suspense fallback={'Die Seite lädt...'}>
<AppContext.Provider value ={{userState, setUserState}}>
<Router history={createBrowserHistory()}>
<Switch>
<Route path='/' exact component={ Home }/>
<Route path='/auth/forgot' exact component = { Forgot } />
<Route path='/auth/:type' exact component={ Auth }/>
// A lot more components
<Route component={ ErrorPage }/>
</Switch>
</Router>
</AppContext.Provider>
</Suspense>
);
}
Each of my components (e.g Home)
// Home.tsx
...
return(
<Frame current='/'>
<section className='home-landingPage'>
...
</Frame>
)
are wrapped in a Frame component
// Frame.tsx
interface FrameProperties {
children: React.ReactNode | React.ReactNode[],
current?: string
}
export default function Frame(props: FrameProperties) {
return (
<div className='frame-container'>
<NavigationBar current={ props.current } />
{ props.children }
<Footer/>
</div>
)
}
which adds a NavigationBar to the component.
In this NavigationBar, I am rendering things like signin/signup button (in case authenticated == false) or signout button, profile picture, level progress (in case authenticated == true).
To ensure that the navigation bar displays the correct information, I use an effect hook, which updates the userStatus.
//Navigation.tsx
import AppContext from '../../../context/appState';
...
export default function NavigationBar(props: NavigationBarProps) {
const {userState, setUserState} = useContext(AppContext)
const updateUser = async () => {
fetchGetOwnUser().then(response => {
if(response.status === 200) {
setUserState({...userState, user: response.data}); // update user
}
}).catch(err => {
console.error(err);
});
console.log("USERSTATE AFTTER: ");
console.log(userState);
}
const updateAuthenticationStatus = async () => {
const accessToken = localStorage.getItem('accessToken');
if(accessToken) {
fetchVerifyToken({token: accessToken})
.then(response => {
if(response.status == 200){
const userId = getTokenPayload(accessToken).sub;
setUserState({authenticated: true, user: userState.user, notificationManager: userState.notificationManager || new NotificationManager(userId)}); //update authentication status of user
}
})
.catch(err => {
console.error(err);
});
console.log("USERSTATE AFTER: ");
console.log(userState);
}
useEffect(() => {
console.log("USERSTATE BEFORE: ");
console.log(userState);
if(userState.authenticated){
updateUser();
}else{
updateAuthenticationStatus();
}
}, []);
}
However, although updateAuthenticationStatus and updateUser are executed succesfully, the userState object does not change. The console shows the following output.
USERSTATE BEFORE: Object { authenticated: false, user: undefined, notificationManager: undefined }
USERSTATE AFTTER: Object { authenticated: false, user: undefined, notificationManager: undefined }
Thanks for any help in advance!

Your code looks fine, you just have written your log statements in spots where they are useless. fetchVerifyToken is asynchronous, so a log statement on the next line will run before fetchVerifyToken is complete, and even if you awaited the promise, userState is a local const and is never going to change.
What you really care about is that the component rerenders with the new value, so put your console.log in the body of the component to verify that it rerenders. For example:
export default function NavigationBar(props: NavigationBarProps) {
const {userState, setUserState} = useContext(AppContext)
console.log('rendered with', userState);
// ...

Related

Make react-router-dom v6 pass path as key to rendered element

I think I may need a paradigm shift in my thinking here, so I'm open to those kinds of answers as well here.
Consider the following simplified example:
export const App = (p: { apiClient: SomeApiClient }) => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts/:postId" element={<Post apiClient={p.apiClient} />} />
</Routes>
);
}
export const Home = () => {
return <h1>Home!</h1>
}
export const Post = (p: { apiClient: SomeApiClient }) => {
const { postId } = useParams();
const [ state, setState ] = useState<PostState>({ status: "loading" });
// When the component mounts, get the specified post from the API
useEffect(() => {
if (state.status === "loading") {
(async () => {
const post = await p.apiClient.getPost(postId);
setState({ status: "ready", post });
})();
}
})
return (
<h2>Posts</h2>
{
state.status === "loading"
? <p>Loading....</p>
: <div className="post">
<h3>{state.post.title}</h3>
<div className="content">{state.post.content}</div>
</div>
}
)
}
export type PostState =
| { status: "loading" }
| { status: "ready"; post: BlogPost };
export type BlogPost = { title: string; content: string };
This works fine the first time, but pretend there's a <Link /> on the page that goes to the next post. When I click that link, the URL changes, but the page content doesn't, because React Router is not actually re-mounting the <Post .../> component. That component correctly receives the updated postId and is re-rendered, but since it doesn't get re-mounted, the useEffect logic doesn't run again and the content stays the same.
I've been solving this very awkwardly by creating intermediary components like so:
export const App = (p: { apiClient: SomeApiClient }) => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts/:postId" element={<PostRenderer apiClient={p.apiClient} />} />
</Routes>
);
}
export const PostRenderer = (p: { apiClient: SomeApiClient }) => {
const { postId } = useParams();
return <Post key={postId} postId={postId} apiClient={p.apiClient} />
}
export const Post = (p: { postId: string; apiClient: SomeApiClient }) => {
// ....
}
But I'm starting to get a lot of those, and literally all they do is take the param from the URL and use it as a key on the actual target component. I've read through the react-router-dom docs and am not finding anything that indicates there's a way to automate this. I must be thinking about this wrong.... Any suggestions are appreciated.
I think a more common and practical solution is to add the postId as a dependency to the useEffect to rerun the asynchronous logic when the route param changes.
Example:
export const Post = (p: { apiClient: SomeApiClient }) => {
const { postId } = useParams();
const [state, setState] = useState<PostState>({ status: "loading" });
// When the post id updates, get the specified post from the API
useEffect(() => {
const fetchPostById = async (postId) => {
setState({ status: "loading" });
const post = await p.apiClient.getPost(postId);
setState({ status: "ready", post });
};
fetchPostById(postId);
}, [postId]);
return (
<h2>Posts</h2>
{
state.status === "loading"
? <p>Loading....</p>
: <div className="post">
<h3>{state.post.title}</h3>
<div className="content">{state.post.content}</div>
</div>
}
)
};

Component Re-renders after clicking NavLink & loads same data Multiple times

I have made a sample e-commerce site for practice & I used React, Redux & Router-DOM. For the first time, everything loads perfectly. it has two routes ScreenShot-Link. Home route takes me to the Home page where the problem occurs & the Upload route takes me to the upload page. But when I again go back to HomeRoute the data which was fetched from an API doesn't re-render instead the state gets doubled I mean the existing state remains unchanged but for some reason, the Data gets fetched again and the elements get rendered for 2 times ScreenShot-2-Link.
Body Element which gets doubled if I route back to it from another route...
const Body = ({getProducts, deleteProduct, productState})=>{
const {products, loading, error} = productState;
useEffect(()=>{
getProducts()
}, [getProducts])
const deleteProducts = (id)=>{
deleteProduct(id)
}
return (
<div className="display-flex width-full flex-wrap justify-content-center">
{!loading && !error && products?products.map(product=>(
product.data?product.data.map(d=>(
<BodyDiv
key={uuid.v4()}
imgSrc={d.imgSrc}
title={d.title}
details={d.details}
description={d.description}
onClick={()=>deleteProducts(d._id)}
/>
)):product.map(d=>(
<BodyDiv
key={uuid.v4()}
imgSrc={d.imgSrc}
title={d.title}
details={d.details}
description={d.description}
onClick={()=>deleteProducts(d._id)}
/>
))
))
: loading&&!error
? <div className="vertical-center-strict horizontal-center-strict">Loading.......</div>
: <div className="vertical-center-strict horizontal-center-strict">No Internet!</div>
}
</div>
)
}
const BodyDiv = (props)=>{
return(
<div className="container-div width-full card tiny-padding align-self-start">
<img title={props.title} className="img-responsive hover-filter" src={props.imgSrc}/>
<h2 className="text-align-center">{props.title}</h2>
<h5>{props.details}</h5>
<p className="text-align-justify">{props.description}</p>
<button
className="btn btn-danger"
onClick={props.onClick}
>Delete</button>
</div>
)
}
BodyDiv.propTypes = {
title: PropTypes.string.isRequired,
img: PropTypes.string,
details: PropTypes.string.isRequired,
description: PropTypes.string,
onClick: PropTypes.func
}
const mapStateToProps = (state)=>({
productState: state.productState
})
const conn = connect(mapStateToProps, {getProducts, deleteProduct})(Body)
export default conn;
Router Component & Nav Component
//Parent Router Comonent
const App = () => {
return (
<Router>
<AppNavBar />
<Switch>
<Route path="/" exact>
<Body />
</Route>
<Route path="/upload">
<Upload />
</Route>
<Route path="*">
<div className="vertical-center-strict top-20">
<h1 className="text-align-center">404</h1>
<h3>Page doesn't exist</h3>
<p>Please give the correct address!</p>
</div>
</Route>
</Switch>
</Router>
);
};
export default App;
//NavLink Components..
const NavBar = ()=>{
return(
<>
<NavLink activeClassName="link" to="/" exact>
<NavItem content="Home" /> //<a> element with content as a prop
<NavLink>
<NavLink activeClassName="link" to="/upload">
<NavItem content="Upload"
</NavLink>
</>
)
}
}
My Redux Code: Store & Reducers
//Store
import {createStore, applyMiddleware} from "redux"
import rootReducer from "./reducers/index"
import thunk from "redux-thunk"
const middleWare = [thunk]
const store = createStore(rootReducer, applyMiddleware(...middleWare))
export default store
//RootReducer
import {
FETCHED_PRODUCTS,
FETCHING_PRODUCTS,
ERROR_GET_PRODUCTS,
DELETED_PRODUCT,
ERROR_DELETING_PRODUCT
} from "../actions/productActions";
const initialState = {
products: [],
loading: true,
error: false,
};
const productReducer = (state = initialState, action) => {
switch (action.type) {
case FETCHING_PRODUCTS:
return {
...state,
loading: true,
};
case FETCHED_PRODUCTS:
return {
...state,
products: state.products.concat(action.payload),
loading: false,
error: false,
};
case ERROR_GET_PRODUCTS:
return {
...state,
loading: false,
};
case DELETED_PRODUCT:
return {
...state,
products: state.products.map((product) =>
product.data.filter((d) => d._id !== action.payload)
),
error: false,
};
case ERROR_DELETING_PRODUCT:
return {
...state,
error: true,
};
default:
return state;
}
};
export default productReducer;
ActionCreators
export const getProducts = (payload)=>(dispatch)=>{
return(
fetch("/api/products")
.then(res=>{
dispatch({
type:FETCHING_PRODUCTS
})
if(!res.ok){
dispatch({
type: ERROR_GET_PRODUCTS
})
}
return res.json();
})
.then(json=>{
if(json)dispatch({
type: FETCHED_PRODUCTS,
payload: json
})
})
.catch(err=>{
console.log("Error!! failed to fetch data: "+ err)
})
)
}
export const deleteProduct = (payload)=>dispatch=>{
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
var requestOptions = {
method: 'DELETE',
headers: myHeaders,
body: JSON.stringify({"_id": payload}),
redirect: 'follow'
};
fetch("/api/product-delete", requestOptions).then(res=>{
dispatch({
type: DELETED_PRODUCT,
payload
})
if(res.status === 501 || res.status === 403){
dispatch({
type: ERROR_DELETING_PRODUCT,
})
}
})
.catch(err=>{
console.log("Failed to Delete")
})
}
Please help me I have been searching for the solution for 4 hours...It would be great if you help me...Thanks in advance
When using the useEffect hook the second parameter are dependencies.. so it calls the function again if the dependencies change. Now since getProductsis a function, it will never get called again. Instead, you should put products in there
useEffect(()=>{
getProducts()
}, [products])
//will refetch data everytime product state is updated.
It is a little bit hard to see, what is going on in the code. But I suspect maybe this is the problem. Otherwise let me know.

Hide Some Element With Local Storage In React

I want to hide some navigation when the user haven't login yet, so I use local storage to save user's id and use if logic to hide and show the navigation, but when i clear the data in local storage and compare it to null, the navigation still showed up.
Here is the code to save data in local storage
loginUser = () => {
Axios.post('http://private-6fdd31-intern1.apiary-mock.com/interns/login', this.state.user)
.then((res) => {
if(res.data.role === "admin")
{
localStorage.setItem("user", res.data.user_id)
this.props.history.push('/member-list');
}
}, (err) => {
console.log(err);
})
}
This is how I compare and clear the data when logout navigation is clicked
handleLogout = () => {
localStorage.clear("user");
}
render() {
return(
<Router>
<Fragment>
<div className="navigation">
{ localStorage.getItem("user") !== null?
<Fragment>
<Link to="/member-list">Member</Link>
<Link to="/override-list">Override</Link>
<Link onClick={this.handleLogout} to="/">Logout</Link>
</Fragment>
: null
}
</div>
<Route path="/" exact component={routeProps => <Login {...routeProps}/>}/>
<Route path="/member-list" component={MemberDashboard}/>
<Route path="/override-list" component={OverrideDashboard}/>
</Fragment>
</Router>
)
}
react native wont call render() method as long as there is no state or props update, you need to call this.forceUpdate() to force a rerender. Documentation: https://facebook.github.io/react/docs/component-api.html like
handleLogout = () => {
localStorage.clear("user");
this.forceUpdate()
}
Maybe use the state to save the user when you set to local storage and same thing when you handleLogout?
if(res.data.role === "admin")
{
this.setState({ user: res.data.user_id})
localStorage.setItem("user", res.data.user_id)
this.props.history.push('/member-list');
}
handleLogout = () => {
localStorage.clear("user");
this.setState({ user: ""})
}
So when state is updated, the component re-renders.

this.props.history.push not re-rendering react component

In my component I use this.props.history.push(pathname:.. search:..) to rerender the component and fetch new data form a third party service. When I first call the page it renders. But when I call history push inside the component the URL updates correctly BUT the component doesn't rerender. I read a lot but couldn't get it working. Any ideas?
I'm using react router v4
//index.js
<Provider store={store}>
<BrowserRouter>
<Switch>
<Route path="/login" component={Login}/>
<Route path="/" component={Main}/>
</Switch>
</BrowserRouter>
</Provider>
//Main.js
//PropsRoute is used to push props to logs component so I can use them when fetching new data
const PropsRoute = ({ component: Component, ...rest }) => {
return (
<Route {...rest} render={props => <Component {...props} />}/>
);
};
class Main extends Component {
render() {
return (
<div className="app">
<NavigationBar/>
<div className="app-body">
<SideBar/>
<Switch>
<PropsRoute path="/logs" component={Log}/> //this component is not rerendering
<Route path="/reports" component={Reports}/>
<Route path="/gen" component={Dashboard}/>
<Redirect from="/" to="/gen"/>
</Switch>
</div>
</div>
)
}
}
export default Main;
//inside 'Log' component I call
import React, {Component} from 'react';
import {getSystemLogs} from "../api";
import {Link} from 'react-router-dom';
import _ from "lodash";
import queryString from 'query-string';
let _isMounted;
class Log extends Component {
constructor(props) {
super(props);
//check if query params are defined. If not re render component with query params
let queryParams = queryString.parse(props.location.search);
if (!(queryParams.page && queryParams.type && queryParams.pageSize && queryParams.application)) {
this.props.history.push({
pathname: '/logs',
search: `?page=1&pageSize=25&type=3&application=fdce4427fc9b49e0bbde1f9dc090cfb9`
});
}
this.state = {
logs: {},
pageCount: 0,
application: [
{
name: 'internal',
id: '...'
}
],
types: [
{
name: 'Info',
id: 3
}
],
paginationPage: queryParams.page - 1,
request: {
page: queryParams.page === undefined ? 1 : queryParams.page,
type: queryParams.type === undefined ? 3 : queryParams.type,
pageSize: queryParams.pageSize === undefined ? 25 : queryParams.pageSize,
application: queryParams.application === undefined ? 'fdce4427fc9b49e0bbde1f9dc090cfb9' : queryParams.application
}
};
this.onInputChange = this.onInputChange.bind(this);
}
componentDidMount() {
_isMounted = true;
this.getLogs(this.state.request);
}
componentWillUnmount() {
_isMounted = false;
}
getLogs(request) {
getSystemLogs(request)
.then((response) => {
if (_isMounted) {
this.setState({
logs: response.data.Data,
pageCount: (response.data.TotalCount / this.state.request.pageSize)
});
}
});
}
applyFilter = () => {
//reset page to 1 when filter changes
console.log('apply filter');
this.setState({
request: {
...this.state.request,
page: 1
}
}, () => {
this.props.history.push({
pathname: '/logs',
search: `?page=${this.state.request.page}&pageSize=${this.state.request.pageSize}&type=${this.state.request.type}&application=${this.state.request.application}`
});
});
};
onInputChange = () => (event) => {
const {request} = this.state; //create copy of current object
request[event.target.name] = event.target.value; //update object
this.setState({request}); //set object to new object
};
render() {
let logs = _.map(this.state.logs, log => {
return (
<div className="bg-white rounded shadow mb-2" key={log.id}>
...
</div>
);
});
return (
<main className="main">
...
</main>
);
}
}
export default Log;
Reactjs don't re-run the constructor method when just props or state change, he call the constructor when you first call your component.
You should use componentDidUpdate and do your fetch if your nextProps.location.pathname is different than your this.props.location.pathname (react-router location)
I had this same issue with a functional component and I solved it using the hook useEffect with the props.location as a dependency.
import React, { useEffect } from 'react';
const myComponent = () => {
useEffect(() => {
// fetch your data when the props.location changes
}, [props.location]);
}
This will call useEffect every time that props.location changes so you can fetch your data. It acts like a componentDidMountand componentDidUpdate.
what about create a container component/provider with getderivedstatefromprops lifecycle method, its more react-look:
class ContainerComp extends Component {
state = { needRerender: false };
static getderivedstatefromprops(nextProps, nextState) {
let queryParams = queryString.parse(nextProps.location.search);
if (!(queryParams.page && queryParams.type && queryParams.pageSize && queryParams.application)) {
return { needRefresh: true };
} else {
return { needRefresh: false };
}
}
render() {
return (
<div>
{this.state.needRefresh ? <Redirect params={} /> : <Log />}
</div>
);
}
}

Code splitting route components wrapped in a HOC with React Loadable

I am running into problems using React Loadable with route based code splitting using Webpack 3.11.
When I try to render my app on the server my async modules immediately resolve without waiting for the promise. Thus the SSR output becomes <div id="root"></div>.
App.js:
const App = () => (
<Switch>
{routes.map((route, index) => (
<Route key={index} path={route.path} render={routeProps => {
const RouteComponent = route.component
return <RouteComponent {...routeProps} />
}} />
))}
</Switch>
)
I've defined my async route components with React Loadable like this:
Routes.js
function Loading ({ error }) {
if (error) {
return 'Oh nooess!'
} else {
return <h3>Loading...</h3>
}
}
const Article = Loadable({
loader: () => import(/* webpackChunkName: "Article" */ '../components/contentTypes/Article'),
loading: Loading
})
const Page = Loadable({
loader: () => import(/* webpackChunkName: "Page" */ '../components/contentTypes/Page'),
loading: Loading,
render (loaded, props) {
let Component = WithSettings(loaded.default)
return <Component {...props}/>
}
})
export default [
{
path: `/:projectSlug/:env${getEnvironments()}/article/:articleSlug`,
component: Article,
exact: true
},
{
path: `/:projectSlug/:env${getEnvironments()}/:menuSlug?/:pageSlug?`,
component: Page
}
]
WithSettings.js
export default (WrappedComponent: any) => {
class WithSettings extends React.Component<WithSettingsProps, WithSettingsState> {
static displayName = `WithSettings(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`
state = {
renderWidth: 1200
}
componentDidMount () {
this.loadSettings({ match: { params: { projectSlug: '', env: '' } } })
window.addEventListener('resize', this.onResize)
this.onResize()
}
componentWillUnmount () {
if (isClient) {
window.removeEventListener('resize', this.onResize)
}
}
componentDidUpdate (oldProps) {
this.loadSettings(oldProps)
}
onResize = () => {
this.setState({ renderWidth: this.getLayoutWidth() })
}
getLayoutWidth () {
return (document.body && document.body.offsetWidth) || 0
}
loadSettings (oldProps) {
const { settings, request, getNewSettings } = this.props
const { projectSlug: oldProjectSlug, env: oldEnv } = oldProps.match.params
const { projectSlug: newProjectSlug, env: newEnv } = this.props.match.params
if (
(
oldProjectSlug !== newProjectSlug ||
oldEnv !== newEnv
) ||
(
settings === undefined ||
(request.networkStatus === 'ready')
)
) {
getNewSettings()
}
}
render () {
const { settings, request, history, location, match } = this.props
const { renderWidth } = this.state
if (!settings || !request || request.networkStatus === 'loading') {
return <div />
}
if (request.networkStatus === 'failed') {
return <ErrorBlock {...getErrorMessages(match.params, 'settings')[4044]} fullscreen match={match} />
}
return (
<WrappedComponent
settings={settings}
settingsRequest={request}
history={history}
location={location}
match={match}
renderWidth={renderWidth}
/>
)
}
}
hoistNonReactStatic(WithSettings, WrappedComponent)
return connect(mapStateToProps, mapDispatchToProps)(WithSettings)
}
I've managed to narrow it down to the WithSettings HOC that I am using to wrap my route components in. If I don't use the WithSettings HOC (as with the Article route) then my SSR output waits for the async import to complete, and the server generated html includes markup related to the route (good!). If I do use the HOC (as with the Page route) then the module immediately resolves and my SSR output turns into <div id="root"></div because it no longer waits for the dynamic import to complete before rendering. Problem is: I need the WithSettings HOC in all my routes as it fetches required data from the server that I need to render the app.
Can anyone tell me how I can use a HOC and use React Loadable's async component for route components so that it works on the server?
Managed to figure it out.
It was due to my HOC. In the render method it would return <div /> when settings or request where undefined or request.networkStatus is loading. It turns out this tripped up server side rendering. Removing this block was enough to make it work.

Resources