React useContext not triggering rerender in child after provider state is updated - javascript

I'm having trouble getting my child component to rerender when using a context provider. Logging the listener output shows that the AuthContextProvider component does successfully update and connect to the authentication service, but the child App component does not rerender - the screen shows that I am not logged in.
Any help would be greatly appreciated.
AuthContext.tsx:
import React, {createContext, useEffect, useState} from 'react';
import firebase from 'firebase';
import firebaseConfig from '../config/firebaseConfig';
export type AuthContextProps = {
user: firebase.User | null;
authenticated: boolean,
setUser: any,
loadingAuthState: boolean
}
const defaultAuthContext: AuthContextProps = {
user: null,
authenticated: false,
setUser: null,
loadingAuthState: true
}
let AuthContext = createContext(defaultAuthContext);
const AuthProvider = (props: any) => {
const AuthContext = React.createContext<Partial<AuthContextProps>>({});
const [user, setUser] = useState(null as firebase.User | null);
const [loadingAuthState, setLoadingAuthState] = useState(true);
useEffect(() => {
const fetchUser = async () => {
firebase.initializeApp(firebaseConfig).firestore();
await firebase.auth().signInWithEmailAndPassword('test#gmail.com', 'gympass').then(val => val.user);
}
fetchUser().then(() => {
firebase.auth().onAuthStateChanged((user: any) => {
setUser(user)
console.log('LOGGED IN')
setLoadingAuthState(false)
});
});
}, [])
const value = {
user,
authenticated: user !== null,
setUser,
loadingAuthState
}
console.log('VALUE:', value)
return (
<AuthContext.Provider value={value}>
{props.children}
</AuthContext.Provider>
)
}
const AuthConsumer = AuthContext.Consumer;
export {AuthContext, AuthProvider, AuthConsumer}
App.tsx:
import React, {useContext, useEffect, useState} from 'react';
import {AuthContext} from './context/AuthContext';
import {AuthContextProps} from "./context/AuthContext";
function App() {
const auth: AuthContextProps = useContext(AuthContext)
return (
<div className="App">
<header className="App-header">
<p>You are currently {auth.user === null ? 'not ' : ''}logged in.</p>
</header>
</div>
);
}
export default App;
index.tsx:
import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import {AuthProvider} from './context/AuthContext'
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>,
document.getElementById('root')
);
serviceWorker.unregister();

Related

Changing Navbar depending on globalstate boolean gives white screen

So I was following this tutorial to use global state through react context api.
I want to change the navbar based on if the user is logged in, where I need to use global state. But I am getting a white screen so I assume I did something wrong.
AppContext.js
import React from "react";
// ./components/AppContext.js
const AppContext = React.createContext();
export default AppContext;
Index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals';
import { CookiesProvider } from 'react-cookie';
import App from "./App";
import AppContext from "./components/AppContext";
// index.js
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<CookiesProvider>
<App/>
</CookiesProvider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more:
reportWebVitals();
App.js
import React, {useState} from 'react';
import './App.css';
import Home from './Home';
import {BrowserRouter as Router, Route, Routes} from 'react-router-dom';
import User from "./User";
import NavBarSI from "./components/NavBarSignedIn";
import NavBarGL from "./components/NavBarGeneral";
import AppContext from "./components/AppContext";
//App.js
const App = () => {
const userSettings = {
setting2name: setting2value,
setSetting2value
};
const [setting2value, setSetting2value] = useState(false);
const navbar = setting2value ?
<NavBarSI/> : <NavBarGL/>
return (
<Router>
<AppContext.Provider value={userSettings}>
{navbar}
<NavBarSI/>
<div>
<Routes>
<Route exact path="/" element={<Home/>}/>
<Route exact path="/user" element={<User/>}/>
</Routes>
</div>
</AppContext.Provider>
</Router>
);
}
export default App;
Home.js example where I change the boolean to true when user logs in.
import React, {useContext, useEffect, useState} from 'react';
import './App.css';
import { Button, Container } from 'reactstrap';
import { useCookies } from 'react-cookie';
import AppContext from "./components/AppContext";
// Home.js
const Home = () => {
const myContext = useContext(AppContext);
const [authenticated, setAuthenticated] = useState("authenticated");
const [loading, setLoading] = useState(false);
const [user, setUser] = useState(undefined);
const [cookies] = useCookies(['XSRF-TOKEN']);
useEffect(() => {
setLoading(true);
fetch('/user', { credentials: 'include' })
.then(response => response.text())
.then(body => {
if (body === '') {
setAuthenticated(false);
} else {
setUser(JSON.parse(body));
setAuthenticated(true);
myContext.setSetting2value(true);
}
setLoading(false);
});
}, [setAuthenticated, setLoading, setUser])
const login = () => {
let port = (window.location.port ? ':' + window.location.port : '');
if (port === ':3000') {
port = ':8080';
}
window.location.href = `//${window.location.hostname}${port}/private`;
}
const logout = () => {
fetch('/user/logout', {
method: 'POST', credentials: 'include',
headers: { 'X-XSRF-TOKEN': cookies['XSRF-TOKEN'] }
})
.then(res => res.json())
.then(response => {
window.location.href = `${response.logoutUrl}&returnTo=${window.location.origin}`;
});
}
const message = user ?
<h2>Welcome, {user.name}!</h2> :
<p>Please log in to manage your JUG Tour.</p>;
const button = authenticated ?
<div>
<br/>
<Button color="link" onClick={logout}>Logout</Button>
</div> :
<Button color="primary" onClick={login}>Login</Button>;
if (loading) {
return <p>Loading...</p>;
}
return (
<div>
<Container fluid>
{message}
{button}
</Container>
</div>
);
}
export default Home;
Check your console.
You getting white screen because trying to assign variable before its declaration.
Swap variables.
Should do the trick
const [setting2value, setSetting2value] = useState(false);
const userSettings = {
setting2name: setting2value,
setSetting2value
};
Furthermore i suggest to memoize your context object to prevent additional re-renders using useMemo.
Like this:
const context = useMemo(() => {
const ctx = { setting2name: setting2value,setSetting2value}
return ctx;
}, [setting2name]);
Hope it helps!

React: How can I make a component wait for a value in my case

The problem:
So basically I created a context which is used to store the logged in user data so I can easily use it accross my application. I created a component which verify if the user is logged in or not, or if the access to the content of it is restricted to administrator only. The problem occurs when I try to access a page which uses SecureCard. It basically says that user is empty so it just redirect me to the home page, but it's clearly not empty because I can see my username in the header of my website. So my question is why is that.
If you need any extra code or some more context just tell me.
Some context:
import React from 'react';
import ReactDOM from 'react-dom';
import App from "./App";
import {UserProvider} from "./contexts/UserProvider";
ReactDOM.render(
<React.StrictMode>
<UserProvider>
<App/>
</UserProvider>
</React.StrictMode>,
document.getElementById('root')
);
Where I think it originates from:
import React, {useEffect, useState} from "react";
import {ReactSession} from "react-client-session";
import {getUserByToken} from "../utils/accounts";
const UserContext = React.createContext(null);
export function UserProvider({children}) {
const [user, setUser] = useState({
_id: "",
creation_date: "",
rank: "",
username: ""
});
useEffect(() => {
if (ReactSession.get("token") && (user._id === "" || user.rank === "" || user.username === "" || user.creation_date === "")) {
getUserByToken().then(resp => {
if (!resp?.error) setUser(resp.message);
else ReactSession.remove("token");
});
}
}, []);
return (
<UserContext.Provider value={{user, setUser}}>
{children}
</UserContext.Provider>
);
}
export const UserConsumer = UserContext.Consumer;
export default UserContext;
Where the problem happens:
import {Card} from "react-bootstrap";
import {useEffect, useState} from "react";
import {ReactSession} from "react-client-session";
import {useNavigate} from "react-router-dom";
import useUser from "../hooks/useUser";
export function SecureCard(props) {
const navigate = useNavigate();
const [valid, setValid] = useState(false);
const {user} = useUser();
//Prevent the user from accessing the card if connected or not or if restricted
useEffect(() => {
console.log(user);
if (props.connected && props.restricted){
if (!ReactSession.get("token") || user.rank !== "Administrator") navigate("/");
}
else if (props.connected){
if (!ReactSession.get("token")) navigate("/");
}
else if (!props.connected){
if (ReactSession.get("token")) navigate("/");
}
setValid(true);
}, []);
if(!valid)
return null;
return (
<Card>
<Card.Header><h2>{props.title}</h2></Card.Header>
<Card.Body>
{props.children}
</Card.Body>
</Card>
);
}
How SecureCard is called
<SecureCard title="Web Admin" connected={true} restricted={true}></SecureCard>

Type Error: Cannot read property of undefined in react js

I'm trying to use hooks but, I'm not able to figure out the problem which is appearing. I want to set the token value in the useEffect but Im not able to define it.
import React, { useEffect } from "react";
import "./App.css";
import Login from "./Login";
import { getTokenFromUrl } from "./spotify";
import SpotifyWebApi from "spotify-web-api-js";
import Player from "./Player";
import { useDataLayerValue } from "./DataLayer";
const spotify = new SpotifyWebApi();
function App() {
const [{ token }, dispatch] = useDataLayerValue();
useEffect(() => {
const hash = getTokenFromUrl();
window.location.hash = "";
let _token = hash.access_token;
if (_token) {
dispatch({
type: "SET_TOKEN",
token: _token,
});
spotify.setAccessToken(_token);
spotify.getMe().then((user) => {
dispatch({
type: "SET_USER",
user: user,
});
});
}
console.log(token);
}, [dispatch, token]);
return <div className="App">{token ? <Player /> : <Login />}</div>;
}
export default App;
import React, { createContext, useContext, useReducer } from "react";
export const DataLayerContext = createContext();
export const DataLayer = ({ reducer, inititalState, children }) => (
<DataLayerContext.Provider value={useReducer(reducer, inititalState)}>
{children}
</DataLayerContext.Provider>
);
export const useDataLayerValue = () => useContext(DataLayerContext);
export const initialState = {
user: null,
playlists: [],
playing: false,
item: null,
token: null,
};
const reducer = (state, action) => {
switch (action.type) {
case "SET_USER":
return {
...state,
user: action.user,
};
case "SET_TOKEN":
return {
...state,
token: action.token,
};
default:
return state;
}
};
export default reducer;
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { DataLayer } from "./DataLayer";
import reducer, { initialState } from "./reducer";
ReactDOM.render(
<React.StrictMode>
<DataLayer initialState={initialState} reducer={reducer}>
<App />
</DataLayer>
</React.StrictMode>,
document.getElementById("root")
);
It is throwing the following error: TypeError: Cannot read property 'token' of undefined.
Im not able to understand why it is saying undefined. Can anyone please help me to do this.
The problem is that there appears to be no DataLayer being rendered, thus DataLayerContext remains undefined and then useDataLayerValue() returns undefined.
Long story short: You need to wrap your App component in a DataLayer and/or change export const DataLayerContext = createContext(<Default value with a token>)

my react code is working but when i refresh the page i get TypeError: Cannot read property 'Location' of undefined

Starting with GamePage, it provides 2 routes which renders the components GameList and GameDetailPage. Both work fine at first but When i refresh the page for Gamelist component, it still rerenders the page but when i refresh the page for GameDetailPage, i get the error TypeError: Cannot read property 'Location' of undefined. I do not understand why it is unable to fetch data from state whenever i refresh.
gamepage.jsx
import React from "react";
import GamesList from "../../components/games-list/game-list.component";
import { Route } from "react-router-dom";
import GameDetailPage from "../gamedetailpage/gamedetailpage.component";
import {firestore,convertCollectionsSnapshotToMap} from '../../firebase/firebase.utils'
import {connect} from 'react-redux'
import {updateFootballGames} from '../../redux/games/games.actions'
class GamePage extends React.Component {
unsubscribeFromSnapshot=null;
//whenever the component mounts the state will be updated with the football games.
componentDidMount(){
const {updateFootballGames}=this.props
const gameRef=firestore.collection('footballgames')
gameRef.onSnapshot(async snapshot=>{
const collectionsMap=convertCollectionsSnapshotToMap(snapshot)
updateFootballGames(collectionsMap)
})
}
render() {
const { match } = this.props;
return (
<div className="game-page">
<h1>games page</h1>
<Route exact path={`${match.path}`} component={GamesList} />
<Route path={`${match.path}/:linkUrl`} component={GameDetailPage}
/>
</div>
);
}
}
const mapStateToProps=state=>({
games:state.games.games
})
const mapDispatchToProps=dispatch=>({
updateFootballGames:collectionsMap=>
dispatch(updateFootballGames(collectionsMap))
})
export default connect(mapStateToProps, mapDispatchToProps)(GamePage);
gamedetailpage.component.jsx
import React from "react";
import { connect } from "react-redux";
import GamePreview from '../../components/game-preview/game-preview.component'
import GameDetails from '../../components/game-details/game-details.component'
const GameDetailPage = (props) => {
const {games, match} = props
const urlparam =match.params.linkUrl
// const games_array = Object.entries(games)
const gameObj=games[urlparam]
console.log('prop',gameObj)
return (
<div className="game-list">
<GameDetails game = {gameObj}/>
</div>
);
};
const mapStateToProps = (state) => ({
games: state.games.games,
});
export default connect(mapStateToProps)(GameDetailPage);
game_details.component.jsx
import React from 'react';
const GameDetails = (props) => {
console.log(props.game.Location)
return(
<div>
Location:{props.game.Location}
<br/>
Price:{props.game.Price}
</div>
)
}
export default GameDetails;
gamelist.component.jsx
import React from "react";
import './game-list.styles.scss'
import GamePreview from "../game-preview/game-preview.component";
import {connect} from 'react-redux'
const GameList=(props)=>{
const {games}=props
console.log(games)
const game_list=Object.entries(games)
console.log(game_list)
return (
<div className="game-list">
{game_list.map(game =>
<GamePreview game = {game[1]}/>)}
</div>
);
}
const mapStateToProps=state=>({
games:state.games.games
})
export default connect(mapStateToProps)(GameList);
gamepreview.component.jsx
import React from "react";
import "./game-preview.styles.scss";
import { withRouter, Route } from "react-router-dom";
import GamePreviewDetail from "../game-preview-detail/game-preview-detail.component";
const GamePreview = (props) => {
const { Location, Time, linkUrl, Price } = props.game;
const { history, match } = props;
return (
<div
className="game-preview"
onClick={() => history.push(`${match.url}/${linkUrl}`)}
>
<div className="game-preview-image">
<p>Picture goes here</p>
</div>
{/* <GamePreviewDetail name = {Location} price={Price}/> */}
<p>Location:{Location}</p>
<p>Price:{Price}</p>
</div>
);
};
export default withRouter(GamePreview);
app.js
import React from 'react';
import './App.css';
//import dependencies
import { Route, Switch } from "react-router-dom";
//import pages
import HomePage from './pages/homepage/homepage'
import GamesPage from './pages/gamespage/gamespage'
import SignInSignUp from './pages/signin-signup-page/signin-signup-page'
import GameDetailPage from "./pages/gamedetailpage/gamedetailpage.component";
import Header from './components/header/header.component';
import { auth, createUserProfileDocument } from './firebase/firebase.utils';
class App extends React.Component{
constructor() {
super();
this.state = {
currentUser: null
}
}
unsubscribeFromAuth = null
componentDidMount() {
this.unsubscribeFromAuth = auth.onAuthStateChanged(async userAuth => {
if (userAuth) {
const userRef = await createUserProfileDocument(userAuth);
// check if the snapshot has changed (subscribe)
// get the user that we just created or that already exists in the db
userRef.onSnapshot(snapshot => {
this.setState({
currentUser: {
id: snapshot.id,
...snapshot.data()}
})
})
} else {
this.setState({currentUser: userAuth})
}
})
}
componentWillUnmount() {
this.unsubscribeFromAuth();
}
render(){
return(
<div>
<Header currentUser = {this.state.currentUser}/>
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/games" component={GamesPage} />
<Route exact path="/signin" component={SignInSignUp} />
</Switch>
</div>
)
}
}
export default App;
I would try using useParams hook instead. Then capturing any changes of linkUrl with useEffect hook. Also introducing gameObj with useState.
useParams returns an object of key/value pairs of URL parameters. Use it to access match.params of the current <Route>.
If you're familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
Try to modify <GameDetailPage /> component as the following:
import React, { useState, useEffect } from 'react';
import { useParams } from "react-router-dom";
// other imports
const GameDetailPage = (props) => {
const { games } = props;
let { linkUrl } = useParams();
const [ gameObj, setGameObj ] = useState(null);
useEffect(() => {
if (games) {
const newGameObj = games[linkUrl];
console.log('game object', newGameObj);
setGameObj(newGameObj);
}
}, [games, linkUrl]);
return <div className="game-list">
{ gameObj && <GameDetails game={ gameObj } /> }
</div>
}
+1 - null check:
Also you can see a null check in the return statement for gameObj which helps rendering only that case once you have a value in games array with found linkUrl value.
I hope this helps!

React Hooks with React Router v4 - how do I redirect to another route?

I have a simple react hooks application - a list of Todos - with react router v4
On the List of Todos, when a Todo is clicked I need to:
Dispatch the current todo in context
Redirect to another route (from /todos to /todos/:id)
In the previous React Class based implementation I could use this.context.history.push to redirect to another route.
How would I handle that using React Hooks in combination of React Router v4 (in code below see my comment in function editRow())?
Code below:
=====index.js=====
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter} from "react-router-dom"
import App from './App';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>, document.getElementById('root'));
=====main.js=====
import React from 'react'
import { Switch, Route } from 'react-router-dom'
import TodosList from './todoslist'
import TodosEdit from './todosedit'
const Main = () => (
<main>
<Switch>
<Route exact path="/todos" component={TodosList}/>
<Route exact path="/todos/:id" component={TodosEdit} />
</Switch>
</main>
)
export default Main
=====app.js=====
import React, {useContext, useReducer} from 'react';
import Main from './main'
import TodosContext from './context'
import todosReducer from './reducer'
const App = () => {
const initialState = useContext(TodosContext);
const [state, dispatch] = useReducer(todosReducer, initialState);
return (
<div>
<TodosContext.Provider value={{state, dispatch}}>
<Main/>
</TodosContext.Provider>
</div>
)
}
export default App;
=====TodosContext.js=====
import React from 'react'
const TodosContext = React.createContext({
todos: [
{id:1, text:'Get Grocery', complete:false},
{id:2, text:'Excercise', complete:false},
{id:3, text:'Drink Water', complete:true},
],
currentTodo: {}
})
export default TodosContext
=====reducer.js=====
import React from 'react'
export default function reducer(state, action){
switch(action.type){
case "GET_TODOS":
return {
...state,
todos: action.payload
}
case "SET_CURRENT_TODO":
return {
...state,
currentTodo: action.payload
}
default:
return state
}
}
=====Todos.js=====
import React, {useState, useContext, useEffect} from 'react';
import TodosContext from './context'
function Todos(){
const [todo, setTodo] = useState("")
const {state, dispatch} = useContext(TodosContext)
useEffect(()=>{
if(state.currentTodo.text){
setTodo(state.currentTodo.text)
} else {
setTodo("")
}
dispatch({
type: "GET_TODOS",
payload: state.todos
})
}, [state.currentTodo.id])
const editRow = event =>{
let destUrlEdit = `/todos/${event.id}`
let obj = {}
obj.id = event.id
obj.text = event.text
dispatch({type:"SET_CURRENT_TODO", payload: obj})
//after dispatch I would like to redirect to another route to do the actual edit
//destUrlEdit
}
return(
<div>
<h1>List of ToDos</h1>
<h4>{title}</h4>
<ul>
{state.todos.map(todo => (
<li key={todo.id}>{todo.text}
<button onClick={()=>{
editRow(todo)}}>
</button>
</li>
))}
</ul>
</div>
)
}
export default Todos;
It's actually a lot simpler than the other answers, React Router v5.1 provides a useHistory hook.
import React from 'react'
import { useHistory } from 'react-router-dom'
const MyComponent = () => {
const history = useHistory()
const handleButtonClick = (event) => {
history.push(event.target.value)
}
return (
<button
type="button"
value="/my/path"
onClick={handleButtonClick}
>
Navigate Me!
</button>
)
}
Your problem is related to Programmatically navigating using react-router-v4 instead of with hooks,
In react-router-v4, you would get history from props if the Todos component is rendered as a child or Route or from an ancestor that is render form Route and it passed the Router props to it. However it is not receiving Router props, you can use withRouter HOC from react-router to get the router props and call props.history.push(destUrlEdit)
import React, {useState, useContext, useEffect} from 'react';
import TodosContext from './context'
import { withRouter } from 'react-router-dom';
function Todos(props){
const [todo, setTodo] = useState("")
const {state, dispatch} = useContext(TodosContext)
useEffect(()=>{
if(state.currentTodo.text){
setTodo(state.currentTodo.text)
} else {
setTodo("")
}
dispatch({
type: "GET_TODOS",
payload: state.todos
})
}, [state.currentTodo.id])
const editRow = event =>{
let destUrlEdit = `/todos/${event.id}`
let obj = {}
obj.id = event.id
obj.text = event.text
dispatch({type:"SET_CURRENT_TODO", payload: obj})
//after dispatch I would like to redirect to another route to do the actual edit
//destUrlEdit
props.history.push(destUrlEdit);
}
return(
<div>
<h1>List of ToDos</h1>
<h4>{title}</h4>
<ul>
{state.todos.map(todo => (
<li key={todo.id}>{todo.text}
<button onClick={()=>{
editRow(todo)}}>
</button>
</li>
))}
</ul>
</div>
)
}
export default withRouter(Todos);
You can use UseNavigate to move the change page. here is the sample example
"react-router-dom": "^6.2.1",
// Route File
import React, { Suspense, lazy } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import IndexLayout from "../layouts";
import NotFoundPage from "../views/404";
import Loader from "../components/Loader";
const Dashboard = lazy(() => import("../containers/DashboardContainer"));
const Router = () => {
return (
<BrowserRouter>
<IndexLayout> // this one is kind of HOC
<Routes>
<Route
path="/"
element={
<Suspense fallback={<Loader />}>
<Dashboard />
</Suspense>
}
/>
</end every thing>
// any component
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
const TestComponent = ({ newSignup }) => {
const navigate = useNavigate();
useEffect(() => {
if (newSignup) {
navigate("/login");
}
}, [newSignup]);
return (
<div>
</div>
)
}
export default TestComponent
Using react-redux and connected-react-router...
import {useDispatch } from 'react-redux';
import { push } from 'connected-react-router';
export default () => {
const dispatch = useDispatch();
return (
<Button onClick={() => dispatch(push('/login'))}>
Login
</Button>
);
};

Categories