I am unable to set context state in my app.js, I get empty values in it somehow, but can access it in child component.
I want to set context state in app.js whenever user comes to page so that I can use it throughout the application, for example show different headers based on whether user is logged in or not
SandBox URL as requested -> https://codesandbox.io/s/quizzical-snyder-2qghj?file=/src/App.js
I am following https://upmostly.com/tutorials/how-to-use-the-usecontext-hook-in-react
app.js
// import installed dependencies
import React, { useEffect, useContext } from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
// import custom contexts
import { AuthContext, AuthContextProvider } from './contexts/auth/AuthContext';
// import pages
import Homepage from './pages/homepage/Homepage';
// import components
import Footer from './components/footer/Footer';
import Header from './components/header/Header';
export default function App() {
const [authState, setAuthState] = useContext(AuthContext);
useEffect(() => {
console.log(authState); // prints *{}*
console.log(setAuthState); // prints *() => {}*
const token = localStorage.getItem('token');
const tokenIsExpired = parseInt(localStorage.getItem('tokenIsExpired'));
if (!tokenIsExpired && token.length) {
setAuthState({
userIsLoggedin: true,
fName: 'test fname',
lName: 'test lname',
userName: 'testname'
});
} else {
setAuthState({
userIsLoggedin: false,
fName: '',
lName: '',
userName: ''
});
}
if (tokenIsExpired) {
localStorage.setItem('token', '');
}
}, [authState, setAuthState]);
return (
<Router>
<AuthContextProvider value={[authState, setAuthState]}>
<div className='App'>
<Header />
<Switch>
<Route exact path='/'>
<Homepage />
</Route>
</Switch>
<Footer />
</div>
</AuthContextProvider>
</Router>
);
}
AuthContext.js
import React, { useState, createContext } from 'react';
const AuthContext = createContext([{}, () => {}]);
const AuthContextProvider = (props) => {
const [authState, setAuthState] = useState({
userIsLoggedin: false,
fName: '',
lName: '',
userName: ''
});
return (
<AuthContext.Provider value={[authState, setAuthState]}>
{props.children}
</AuthContext.Provider>
);
};
export { AuthContext, AuthContextProvider };
UseAuthCOntext.js
import { useContext } from 'react';
import { AuthContext } from './AuthContext';
const useAuthContext = () => {
const [authState, setAuthState] = useContext(AuthContext);
const login = (loginDetails) => {
setAuthState({
userIsLoggedin: true,
fName: 'test fname',
lName: 'test lname',
userName: 'testname'
});
};
const logout = () => {
setAuthState({
userIsLoggedin: false,
fName: '',
lName: '',
userName: ''
});
};
return { login, logout };
};
export default useAuthContext;
Header.js
// import installed dependencies
import React, { useContext, useEffect } from 'react';
// import components
import LoggedOutHeader from './logged-out-header/LoggedOutHeader';
import LoggedInHeader from './logged-in-header/LoggedInHeader';
// import custom contexts
import { AuthContext } from '../../contexts/auth/AuthContext';
const Header = () => {
const [authState, setAuthState] = useContext(AuthContext);
console.log(authState); //prints *{userIsLoggedin: false, fName: "", lName: "", userName: ""}*
console.log(setAuthState); //prints *ƒ dispatchAction(fiber, queue, action) {...*
const header = authState.isUserLoggedIn ? (
<LoggedInHeader />
) : (
<LoggedOutHeader />
);
return header;
};
export default Header;
You could use the context provider inside index.js.
ReactDOM.render(
<AuthContextProvider>
<App />
</AuthContextProvider>,
document.getElementById('root')
)
You are passing value to AuthContextProvider, which seems like the value you want to use, and you don't use it.
// value not used inside `AuthContextProvider`
<AuthContextProvider value={[authState, setAuthState]}>
It should be:
const AuthContextProvider = (props) => {
return (
<AuthContext.Provider value={props.value}>
{props.children}
</AuthContext.Provider>
);
};
Your are using the context in the App component, which is not wrapped within AuthContextProvider. In that case the useContext call in the App component will not return the value provided to AuthContextProvider but instead it'll return the "default" values provided to the createContext call.
You need to defer those logic in the App component to a children component within AuthContextProvider.
See note from createContext api:
The defaultValue argument is only used when a component does not have a matching Provider above it in the tree. This can be helpful for testing components in isolation without wrapping them. Note: passing undefined as a Provider value does not cause consuming components to use defaultValue.
Related
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>
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>)
I have a small React hook that is wrapping a class component and I am trying to pass a state from this hook to the class using useEffect but I keep getting the followwing error;
TypeError: Object(...) is not a function or its return value is not iterable on the following line;
const [{}, dispatch] = useStateValue();
I use the same line in another hook elsewhere in my app so I am not sure why this one is throwing this particular error
This is the complete component code.
If anyone has any ideas why this may not be working I would be most appreciative to hear them.
Thank you.
Component
import React, { useEffect } from 'react';
import App from 'next/app';
import firebase from 'firebase';
import { useStateValue, StateProvider } from '../state';
import { firebaseConfig } from '../constants';
import { initialState, reducer } from '../reducer';
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
}
function AppWrapper(Component) {
return function WrappedComponent(props) {
const [{}, dispatch] = useStateValue();
useEffect(() => {
console.log('Moiunted');
firebase.auth().onAuthStateChanged((user) => {
dispatch({
type: 'userLogin',
currentUser: user
})
});
}), [];
return <Component {...props} />
}
}
class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
return (
<StateProvider
initialState={initialState}
reducer={reducer}
>
<Component {...pageProps} />
</StateProvider>
);
}
}
export default AppWrapper(MyApp)
State function
import React, { createContext, useContext, useReducer } from 'react';
export const StateContext = createContext();
export const StateProvider = ({reducer, initialState, children}) => (
<StateContext.Provider
value={useReducer(reducer, initialState)}
>
{children}
</StateContext.Provider>
);
export const useStateValue = () => useContext(StateContext);
I'm new to redux and trying to fetch content from my BackEnd API. For some reason the action I call does not reach the reducer (It's not even executed). I first thought it was because it couldn't access the store since it is has a parent component but my Provider is well configured and there is another component at the same level, and just after i started thinking it was a problem with my dispatch but honestly i don't know. I have attached the code I feel is relevant and any contributions would be highly appreciated.
actions/viewers.js
import axios from 'axios';
import { VIEWERS_LOADED, VIEWERS_ERROR } from './types';
export const loadData = async (body, http) => {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
try {
const res = await axios.post(
http,
body,
config
);
return res.data;
} catch (error) {
console.log(error);
}
};
export const extractConcurrentViewers = (from, to, aggregate) => async dispatch => {
console.log("CONCURRENT VIEWERS");
const body = {
session_token: localStorage.token,
from,
to,
};
try {
let aggregateConcur = null;
const graphConccur = await loadData(body, 'http://localhost:5000/audience');
console.log('extractViews -> res_1', graphConccur);
if (aggregate !== null) {
body.aggregate = aggregate
aggregateConcur = await loadData(body, 'http://localhost:5000/audience');
}
console.log('extractaggregateViewers -> res_2', aggregateConcur);
dispatch({
type: VIEWERS_LOADED,
payload: {
graphConccur,
aggregateConcur
},
});
} catch (error) {
console.log(error);
dispatch({
type: VIEWERS_ERROR,
});
}
}
reducers/viewers.js
import {
VIEWERS_LOADED,
VIEWERS_ERROR,
} from '../actions/types';
const initialState = {
session_token: localStorage.getItem('token'),
concurrence: null,
aggConcurrence: null,
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
case VIEWERS_LOADED:
return {
...state,
...payload,
concurrence: payload.graphConccur.audience,
aggConcurrence: payload.aggregateConcur.audience,
};
case VIEWERS_ERROR:
return {
...state,
concurrence: null,
aggConcurrence: null,
};
default:
return state;
}
}
reducer/index.js
import {combineReducers} from 'redux';
import alert from './alert';
import auth from './auth'
import profile from './profile'
import chart from './chart'
import viewers from './viewers'
export default combineReducers({
alert,
auth,
profile,
chart,
viewers
});
App.js
import React, { useEffect } from 'react';
import Navbar from './components/layout/Navbar';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Landing from './components/layout/Landing';
import Login from './components/auth/Login';
import Register from './components/auth/Register';
import Alert from './components/layout/Alert';
import Dashboard from './components/dashboard/Dashboard';
import PrivateRoute from './components/routing/PrivateRouting';
import { Provider } from 'react-redux';
import store from './store';
import { loadUser } from './actions/auth';
import setAuthToken from './utils/setAuthToken'
import './App.css';
if (localStorage.token) {
setAuthToken(localStorage.token);
}
const App = () => {
useEffect(() => {
store.dispatch(loadUser())
}, []);
return (
<Provider store={store}>
<Router>
<Navbar />
<Route exact path='/' component={Landing} />
<section className='container'>
<Alert />
<Switch>
<Route exact path='/login' component={Login} />
<Route exact path='/register' component={Register} />
<PrivateRoute exact path='/dashboard' component={Dashboard} />
</Switch>
</section>
</Router>
</Provider>
);
};
export default App;
This is where the function extractConcurrentViewers is to be called and the component supposed to use that is <Concurrent concurrence={concurrence}/> and what is really weird about is that the component just above it is implemented almost the same way but it's working.
import React, { useEffect, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import BandWidth from './BandWidth';
import Concurrent from './Concurrent';
import { extractCurrentClient } from '../../actions/profile';
import { extractchartData } from '../../actions/chart';
import { extractConcurrentViewers } from '../../actions/viewers';
const Dashboard = ({
extractCurrentClient,
extractchartData,
auth: { user },
profile: { profile, loading },
chart: { cdn, p2p, maxSum, maxCdn },
viewers: {concurrence}
}) => {
useEffect(() => {
extractCurrentClient();
extractchartData('max', 1585834831000, 1589118031000);
extractConcurrentViewers(1585834831000, 1589118031000);
}, []);
return loading && profile === null ? (
<Spinner />
) : (
<Fragment>
<h1 className='large text-primary'>Streaming</h1>
<p className='lead'>
<i className='fas fa-chart-line'></i>
Welcome {user && user.lname}
</p>
<BandWidth cdn={cdn} p2p={p2p} maxSum={maxSum} maxCdn={maxCdn} />
{/* <Concurrent concurrence={concurrence}/> */}
</Fragment>
);
};
Dashboard.propTypes = {
extractCurrentClient: PropTypes.func.isRequired,
extractchartData: PropTypes.func.isRequired,
extractConcurrentViewers: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
profile: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
auth: state.auth,
profile: state.profile,
chart: state.chart,
viewers: state.viewers,
});
export default connect(mapStateToProps, {
extractCurrentClient,
extractchartData,
extractConcurrentViewers
})(Dashboard);
store.js
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const initialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
You mapped extractConcurrentViewers to props in connect but did not add it to the destructured props object. Since they share the same name, that means is you're calling your action creator without it being bound to dispatch, so it will not be delivered to your reducers.
const Dashboard = ({
extractCurrentClient,
extractchartData,
auth: { user },
profile: { profile, loading },
chart: { cdn, p2p, maxSum, maxCdn },
viewers: {concurrence},
extractConcurrentViewers // <-- add this
}) => {
Personally I don't destructure my props and this is one reason. I prefer the code to be explicit about where values and functions are coming from props.extractConcurrentViewers . But that's my preference.
I'm building an admin app for a project with react, redux, react-router and react-router-redux. React-router is v4.0.0, react-router-redux is v5.0.0-alpha.3 (installed with npm install react-router-redux#next). What I'm trying is:
Load app,
Perform an async call to backend to see if the user is logged in (token stored in a cookie),
If user is not logged in, redirect to /login and render Login component.
For async actions I'm using redux-thunk.
Root.js
import React, { Component, PropTypes } from 'react';
import { Provider, connect } from 'react-redux';
import { Route, Switch } from 'react-router-dom';
import { ConnectedRouter, push } from 'react-router-redux';
import Login from './Login';
const App = () => <h1>Dashboard</h1>;
const NotFound = () => <h1>Not found :(</h1>;
class Root extends Component {
// use componentDidMount as recommended here:
// https://facebook.github.io/react/docs/react-component.html#componentdidmount
componentDidMount() {
const { dispatch, user } = this.props;
if (!user) {
dispatch(push('/login'));
}
}
render() {
const { store, history } = this.props;
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<Switch>
<Route exact path='/' component={App} />
<Route exact path='/login' component={Login} />
<Route component={NotFound} />
</Switch>
</div>
</ConnectedRouter>
</Provider>
);
}
}
Root.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
user: PropTypes.shape({
email: PropTypes.string.isRequired
})
};
const mapStateToProps = state => ({
ready: state.ready,
user: state.user
});
export default connect(mapStateToProps)(Root);
Login.js
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import {
loginFormChange,
loginFormSubmit
} from '../actions';
class Login extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
const { target } = event,
{ value, name } = target,
{ dispatch } = this.props;
dispatch(loginFormChange({
[name]: value
}));
}
handleSubmit(event) {
event.preventDefault();
const { dispatch, login } = this.props,
{ email, password } = login;
dispatch(loginFormSubmit({
email,
password
}));
}
render() {
const { login } = this.props,
{ email, password } = login;
return (
<form onSubmit={this.handleSubmit}>
<input type="email" name="email" value={email} onChange={this.handleChange} />
<input type="password" name="password" value={password} onChange={this.handleChange} />
<button type="submit">Sign in</button>
</form>
);
}
}
Login.propTypes = {
dispatch: PropTypes.func.isRequired,
login: PropTypes.shape({
email: PropTypes.string.isRequired,
password: PropTypes.string.isRequired
}).isRequired
};
const mapStateToProps = state => ({
login: state.login
});
export default connect(mapStateToProps)(Login);
actions.js
export const LOGIN_FORM_CHANGE = 'Login form change';
export const LOGIN_FORM_SUBMIT = 'Login form submit';
export const AUTHENTICATE_USER = 'Authenticate user';
export const loginFormChange = data => {
const { email, password } = data;
return {
type: LOGIN_FORM_CHANGE,
email,
password
};
};
export const loginFormSubmit = data => dispatch => {
const { email, password } = data;
return fetch('/api/auth/token', {
headers: {
'Authorization': 'Basic ' + btoa([ email, password ].join(':'))
},
credentials: 'same-origin'
})
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
})
.then(user => {
// this line will throw setState warning:
// Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.
dispatch(authenticateUser(user));
});
};
export const authenticateUser = data => {
const { email } = data;
return {
type: AUTHENTICATE_USER,
email
};
};
I want to point out that I'm using the recommended approach to async actions, described in redux documentation. I won't post my reducers for brevity. Finally:
index.js
import React from 'react';
import { render } from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import createHistory from 'history/createBrowserHistory';
import { routerMiddleware } from 'react-router-redux';
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';
import reducers from './reducers';
import Root from './containers/Root';
const history = createHistory(),
middleware = [
routerMiddleware(history),
thunk
];
if (process.env.NODE_ENV !== 'production') {
middleware.push(createLogger());
}
const store = createStore(
reducers,
applyMiddleware(...middleware)
);
render(
<Root store={store} history={history} />,
document.getElementsById('root')
);
So the warning gets thrown in the loginFormSubmit async action, when it tries to dispatch a sync authenticateUser action. Moreover it happens only after a redirect. I've tried different redirect approaches:
push from react-router-redux
Redirect component from react-router
I've also tried putting the redirect call in different places (componentWillMount, componentDidMount, componentWillReceiveProps, conditional rendering inside of the component, using conditional PrivateRoute components as described in the react-router documentation, etc.), but nothing seems to work.
If there is no redirect in the first place (e.g. a user opens /login page instead of a protected one), than there is no warning.
Any help on the issue is very much appreciated.
I am having the same issue and basically it's a bug with the ConnectedRouter from react-router-redux v5.0.0-alpha.2 and alpha.3
It was actively being discussed for the past few days but now it's fixed in alpha 4 and the issue is closed:
https://github.com/ReactTraining/react-router/issues/4713