I have create a very simple React/Redux application and I am struggling with getting a simple component to re-render when the state changes. I am using create-react-app as a starting point. In the video series I've been watching by Dan Abramov (https://egghead.io/lessons/react-redux-react-todo-list-example-adding-a-todo), he subscribes a root render function to the store, which gets called each time a change is dispatched. Without using Connect, I'd like to understand how to cause the Value component to re-render on state change.
Thanks!
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import registerServiceWorker from './registerServiceWorker'
import { createStore } from 'redux'
const counter = (state = 0, action) => {
console.log(action)
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
const store = createStore(counter)
ReactDOM.render(<App store={store} />, document.getElementById('root'))
registerServiceWorker()
app.js
import React, { Component } from 'react'
import logo from './logo.svg'
import './App.css'
import Counter from './components/Counter'
class App extends Component {
render() {
return (
<div className="App">
<Counter
value={this.props.store.getState()}
onIncrement={() =>
this.props.store.dispatch({
type: 'INCREMENT'
})
}
onDecrement={() =>
this.props.store.dispatch({
type: 'DECREMENT'
})
}></Counter>
</div>
)
}
}
export default App;
Counter.js
import React from 'react'
import Value from './Value'
const Counter = ({ value, onIncrement, onDecrement }) => {
return (
<div>
<Value value={value}></Value>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
)
}
export default Counter
Value.js
import React from 'react'
const Counter = ({ value }) => {
return (
<h1>{value}</h1>
)
}
export default Counter
Does this help clarify the process?
class App extends Component {
constructor (props) {
super(props)
this.state = { counter: 0 }
const { store } = props
this.unsubscribe = store.subscribe(() => {
const counter = store.getState()
if (counter !== this.state.counter) {
this.setState({ counter })
}
})
}
render () {
return (
<div className='App'>
<Counter
value={this.state.counter}
onIncrement={() =>
this.props.store.dispatch({
type: 'INCREMENT'
})
}
onDecrement={() =>
this.props.store.dispatch({
type: 'DECREMENT'
})
} />
</div>
)
}
}
A few things to note:
This isn't a good solution for anything other than learning. react-redux does this in a much more maintainable and performant way.
Obviously, Redux state would normally be an object and not a single integer.
.subscribe is essentially just a way to set an event listener. When the state updates, any attached handlers will be called. What you do in your handler is pretty much up to you, although sensible precautions against unnecessary rendering would be pretty high on the list.
Without using Connect, I'd like to understand how to cause the Value
component to re-render on state change
First thing we have to do is subscribe to any redux-state changes. We will set the redux-state to your app's local state and update the state every time the redux-state is updated:
/* app.js */
componentDidMount(){
this.props.store.subscribe(() => {
this.setState({reduxState: this.props.store.getState()});
});
}
Then you pass the state to your child components:
render(){
return (
<Counter value={this.state.reduxState} ... />
)
}
Related
I'm using react and react-redux.
I used mapstatetoprops and mapdispatchtoprops to update view of my react component.
Except re-render doesn't work after redux store changed, everything works fine. Action dispatch works fine, reducer works fine, I can console.log store state and check difference.
At first, I used useDispatch and useSelector and everything worked fine. But I'm changing it to mapdispatchtoprops and mapstatetoprops to merge my code into my project teammate's code.
I tried to put this.props.(whatineed) directly in my render()'s return in component. As I understand, through mapstatetoprops, store state should be passed into my component's props.
import React, { Component } from 'react';
import { ToggleButton, ToggleButtonGroup } from 'react-bootstrap';
import { useSelector, useDispatch } from 'react-redux';
import { checked, notchecked } from '../../../actions';
import { connect } from "react-redux";
import local from './address';
import './index.css';
const mapStateToProps = state => {
return {
localsel : state.selectedLocal.locals
}
}
let mapDispatchToProps = (dispatch) => {
return {
check: (btn) => dispatch(checked(btn)),
uncheck: (btn) => dispatch(notchecked(btn))
}
}
class Seoul extends Component {
constructor(props){
super(props)
}
render(){
var btnclicked = (e) => {
let btnname = e.target.parentNode.getAttribute('id');
if (e.target.checked) {
console.log('checked');
this.props.check(btnname);
};
if (!e.target.checked) {
console.log('not checked');
this.props.uncheck(btnname);
};
// HERE IS WHERE I CAN CHECK THE PASSED STORE STATE
console.log(this.props.localsel);
// -------------------------------------------------
}
return (
<div className='localdiv localdiv1'>
// HERE IS WHERE I WANT TO SEE MY STORE STATE
{this.props.localsel.map(val=>{
return <h1>{val}</h1>
})}
// --------------------------------------------
<ToggleButtonGroup className='togglebtngrp' type="checkbox">
<ToggleButton className='togglebtn0' onChange={btnclicked} variant="outline-secondary" value={0} id="entireseoul">Entire Seoul</ToggleButton>
{local.Seoul.map((value, index) => {
return (<ToggleButton key={index} className='togglebtn' onChange={btnclicked} variant="outline-primary" value={index + 1} id={value}>{value}</ToggleButton>)
})}
</ToggleButtonGroup>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Seoul);
this component is exported in parent component, which is
import React, { Component } from 'react';
import { Jumbotron } from 'react-bootstrap';
import { Gyeongi, Incheon, Busan, Daegue, Daejeon, Sejong, Gwangju, Ulsan, Gangwon, Gyungnam, Gyungbuk, Jeonnam, Jeonbuk, Choongnam, Choongbuk, Jeju, Othercountry } from './Locals';
import Seoul from './Locals';
import './Detailsrch.css';
class Detailsrch extends Component{
render(){
var localselect = (e) => {
let selector = document.getElementsByClassName('locals');
let selector_local = document.getElementsByClassName('localdiv');
let i = 0;
for (let j = 0; j < selector_local.length; j++) {
selector_local[j].style.display = 'none';
}
let boxclass = e.target.getAttribute('name');
if (boxclass) document.getElementsByClassName(boxclass)[0].style.display = 'block';
while (selector[i]) {
selector[i].className = 'locals';
i++;
}
if (e.target.className == 'localtext') {
e.target.parentElement.className = 'locals localclick';
} else {
e.target.className = 'locals localclick';
}
}
return (
<Jumbotron className='searchjumbo'>
<p>Locals</p>
<Seoul />
<Gyeongi />
<Incheon />
<Busan />
<Daegue />
<Daejeon />
<Sejong />
<Gwangju />
<Ulsan />
<Gangwon />
<Gyungnam />
<Gyungbuk />
<Jeonnam />
<Jeonbuk />
<Choongnam />
<Choongbuk />
<Jeju />
<Othercountry />
<hr className='firsthr' />
<p>type</p><hr />
<p>career</p><hr />
<p>country</p><hr />
<p>sex</p>
</Jumbotron>
);
}
};
export default Detailsrch;
here's my reducer
import { combineReducers } from 'redux';
const initialstate = {
locals: []
}
const localSelector = (state = initialstate, action) => {
switch(action.type){
case 'CHECKED':
if(action.payload){
var arr = state.locals;
arr.push(action.payload);
return {
...state,
locals: arr
};
} else {
return state;
}
case 'NOTCHECKED':
if(action.payload){
var arrnum = state.locals.indexOf(action.payload);
var arr = state.locals;
arr.splice(arrnum, 1);
return {
...state,
locals: arr
};
} else {
return state;
}
default:
return state;
}
};
const rootReducer = combineReducers({
selectedLocal: localSelector
});
export default rootReducer;
I expect when props value changes, component will re-render and I will see the change in the browser. Props value has changed, but nothing happens in browser.
You are mutating the redux state as below
var arr = state.locals;
arr.push(action.payload);
The redux state should be immutable. You can have a look at here for some tips on how to update the redux store in reducer.
https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns
I can't see Detailsrch imported in Seoul but it's vice versa Seoul is imported in Detailsrch and as per the code and comments i can see this.props.localsel is changing and this is used in Seoul so render method of Seoul will be called and since there is no mapping or usage of this.props.localsel in Detailsrch so render method will not be called.
So if you want to re-render Detailsrch you need to change the mapping of this.props.localsel from Seoul to Detailsrch and pass this value as props to Seoul it should be working.
If still issue exists please post your code to sandbox/codepen/jsfiddle so that we can reproduce.
Probably you are suffering from something like this post mentioned. One trick to make your component to get the changes from the store (When passing state value from the store as a prop) is to make a deep clone of your props (Or the prop in which you want to get the change), for this you could use JSON:
JSON.parse(JSON.stringify(propToClone));
Hope this helps.
P.S.: Don't clone props that are functions, because JSON will erase/ignore them.
I'm having an issue with a React Native application that uses Redux, and I've struggling to figure out the cause.
When the Redux state is changed by a dispatch in component A, component B doesn't show the most up to date value of the Redux state. For example, say I click a button in component A to change a Redux value from 0 to 1, it will show 0 in component B, but when I click the button to add 1 to make it 2, component B will show 1. Below is an example of my code. When I click on the TouchableOpacity in component A, it should change this.props.screen from 1 (the initial state) to 0. In component B, I have a regular console.log of this.props.screen, and a console.log inside a setTimeout with 50 milliseconds. Inside the console, the console.log in the setTimeout has the correct value of 0 when hit, however the one outside it still shows 1. Similarly, the text rendered in component B will show 1 as well. If I click the button again, it will then show 0.
I've included the relevant action and reducer from my code. At first, I thought it might be a mutation, but it seemed that can only happen with objects and arrays (I'm only using a number). I would appreciate some help figuring out how to have the text rendered in component B reflect the most current value. Thanks in advance!
Component A
import { connect } from "react-redux";
import { setScreen } from "../redux/Actions";
class Header extends Component {
componentWillReceiveProps(nextProps){
setTimeout(() => { this.logoHide() },10);
this.props.scrollLocation < 10 ? this.changeTransparency(0) : this.changeTransparency(.9);
}
setScreen(screen){
this.props.setScreen(screen);
}
render() {
var {height, width} = Dimensions.get('window');
return (
<View>
<TouchableOpacity onPress={() => this.setScreen(0)}>
<Text>Click Me</Text>
</TouchableOpacity>
</View>
);
}
}
const mapStateToProps = state => {
return {
height: state.height,
platform: state.platform,
screen: state.screen,
scrollLocation: state.scrollLocation
};
};
const mapDispatchToProps = dispatch => {
return {
setScreen: (value) => dispatch(setScreen(value))
};
};
export default connect(mapStateToProps,mapDispatchToProps)(Header);
Redux Action
import { SET_SCREEN } from './Constants';
export const setScreenDispatcher = (value) => ({ type: SET_SCREEN, screen: value});
export const setScreen = (value) => {
return (dispatch) => {
dispatch(setScreenDispatcher(value));
}
}
Redux Reducer
import { combineReducers } from 'redux';
import { SET_SCREEN } from "./Constants";
const initialState = []
const screen = (state = 1, action) => {
switch (action.type) {
case SET_SCREEN:
return action.screen;
default:
return state;
}
};
// COMBINE REDUCERS //
export default combineReducers({
screen
});
Component B
import { connect } from "react-redux";
class VisibleMenus extends Component {
componentWillUpdate(){
console.log(this.props.screen);
setTimeout(() => {console.log(this.props.screen)},50);
}
}
render() {
return (
<View>
<Text>{this.props.screen}</Text>
</View>
);
}
}
const mapStateToProps = state => {
return {
screen: state.screen
};
};
const mapDispatchToProps = dispatch => {
return {
};
};
export default connect(mapStateToProps,mapDispatchToProps)(VisibleMenus);
App.js
import React, {Component} from 'react';
import { Provider } from "react-redux";
import VisibleMenus from './VisibleMenus';
import { Store } from "./redux/Store";
const store = Store();
export default class App extends Component {
render() {
return (
<Provider store={store}>
<VisibleMenus />
</Provider>
);
}
}
Store.js
// REDUX STORE //
import { createStore, applyMiddleware } from "redux";
import rootReducer from "./Reducers";
import ReduxThunk from 'redux-thunk'
export const Store = (initialState) => {
return createStore(
rootReducer,
initialState,
applyMiddleware(ReduxThunk)
);
}
For anyone who runs into this, I thought I'd share how I fixed it.
I researched mutations, and I definitely wasn't mutating the state, yet my components would not update when the Redux store changed.
I tried using both componentWillUpdate() and componentWillReceiveProps() but both didn't change anything. However, I was doing a comparison between this.props.screen and this.state.screen which ended up being my issue.
I should have been doing a comparison with nextProps.screen and this.state.screen inside a componentWillReceiveProps(nextProps) which ended up fixing everything.
I do want to thank Hashith for his help.
In the below example, I would like component to rerender when list is updated. But even though connect is passed new state, it doesn't rerender the component.
I know that connect performs shallow compare, but don't know how to make it compare the values of object. I couldn't find any example of connect with the options enabled.
I have seen How does a redux connected component know when to re-render? and some more but it doesn't help either.
I have tried
const ConnectList = connect(mapStateToProps,null,null,{areStatesEqual : () => false})(List)
to just try to make it rerender for any change. That doesn't seem to be working as well.
import React, { Component } from 'react';
import ReactDOM from 'react-dom'
import {createStore} from 'redux'
import thunk from 'redux-thunk'
import {connect, Provider} from 'react-redux'
function testReducer (state=null,action) {
console.log(`Reducer: Reducer received action ${action.type}. ${action.comment}`)
switch(action.type){
case 'LIST': {
return ({ ...state, list: action.list })
}
case 'OTHER': {
return ({ ...state, other: action.other })
}
default:
return state
}
}
function testAction() {
return {
type: 'LIST',
list: ['first','second'],
comment: `This will trigger both connect() and mount Component List mount because, both reducer and connect changes state after this action`
}
}
function testActionChange() {
return {
type: 'LIST',
list: ['first','second','third'],
comment: `This will trigger both connect() and mount Component List mount because, both reducer and connect changes state after this action`
}
}
function testOther() {
return {
type: 'OTHER',
other: `some other value`,
comment: `This will trigger connect(), but not mount Component List because the return from connect() doesn't change`
}
}
function inertAction() {
return {
type: 'INERT',
comment: 'This action should not trigger either connect() or mount Component List , because reducer returs the same state'
}
}
const store = createStore(testReducer, [thunk])
store.dispatch(testAction())
//Dispatch an action after 2 secs
setTimeout(store.dispatch.bind(null,testOther()),2000)
setTimeout(store.dispatch.bind(null,inertAction()),4000)
setTimeout(store.dispatch.bind(null,testActionChange()),6000)
class List extends Component {
componentDidMount(){
console.log(`Component List mounted`)
}
render(){
const {list} = this.props
return(
<div>
{list.map((element) => {
return(<Element key={element} element={element} />)
})}
</div>
)
}
}
function mapStateToProps({list}){
console.log(`connect() triggered`)
return( {
list
})
}
const ConnectList = connect(mapStateToProps)(List)
class Element extends Component {
render(){
const {element} = this.props
return(
<div>{element}</div>
)
}
}
ReactDOM.render(<Provider store={store}>
<ConnectList />
</Provider>,
document.getElementById('root')
);
Output
Added console.log in connect.
I don't recognize the syntax your using in mapStateToProps
try:
function mapStateToProps(state){
console.log(`connect() triggered`)
const list = state.list;
return { list };
}
I figured out that connect in fact calls the component. But only the render method. So I had to move my action creator calls at componentDidMount to a middleware in redux to add appropriate dispatcher when state changes.
i have two components one is App component and other is counter component.Here App component is root component and counter component is child component.I used redux in this application.
In index component in subscribe method i am receiving updated value.
Here the problem is in Counter component how to receive update if any changes in redux store.?
//index.js
import React from 'react';
import { render } from 'react-dom';
import Counter from './Counter';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
const initialState = {
count: 0
}
function reducer(state=initialState,action) {
if(action.type === "INCREMENT"){
return [...state,{count: state.count + 1}]
}
if(action.type === "DECREMENT"){
return [...state,{count: state.count - 1}]
}
return state;
}
export const store = createStore(reducer);
store.subscribe(() =>{
console.log("state value",store.getState()[0].count);
})
const App = () => (
<Provider store={store}>
<Counter
/>
</Provider>
);
render(<App />, document.getElementById('root'));
--------------------------
//Counter.js
import React from 'react';
import { connect } from 'react-redux';
import {store} from './index';
class Counter extends React.Component {
increment(){
store.dispatch({type: 'INCREMENT',payload: 1})
}
decrement(){
store.dispatch({type: 'DECREMENT',payload: 1});
}
componentWillReceiveProps(){
console.log("Props",this.props)
}
render() {
return (
<div>
<h2>Counter</h2>
<div>
<button onClick={this.decrement.bind(this)}>-</button>
<span>{this.props.count}</span>
<button onClick={this.increment.bind(this)}>+</button>
</div>
</div>
)
}
}
const mapStateToProps = state => (
{count: state.count}
);
export default connect(mapStateToProps)(Counter);
I suppose you don't need to return an array in reducer as the initial state of youer app is object. You have to just "return a new object". And remember to never mutate the state in reducer as it was designed to be a pure function that means without any side effects.
You also need to add mapDispatchToProps as you use action dispatchers in your Counter component, so that connect(mapStateToProps, mapDispatchToProps)(Counter)
I'm trying to use React's context feature to maintain information about the user throughout the application (e.g. the user ID, which will be used in API calls by various pages). I'm aware that this is an undocumented and not recommended over Redux, but my application is pretty simple (so I don't want or need the complexity of Redux) and this seems like a common and reasonable use case for context. If there are more acceptable solutions for keeping user information globally throughout the application, though, I'm open to using a better method.
However, I'm confused about how it's to be used properly: once the user logins in through the AuthPage (a child of the ContextProvider), how do I update the context in ContextProvider so it can get to other components, like the FridgePage? (Yes, context is technically not supposed to be updated, but this is a one-time operation -- if anyone knows a way to do this when ContextProvider is initialized, that would be more ideal). Does the router get in the way?
I've copied the relevant components here.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Route, Switch } from 'react-router-dom';
import Layout from './components/Layout.jsx';
import AuthPage from './components/AuthPage.jsx';
import ContextProvider from './components/ContextProvider.jsx';
ReactDOM.render(
<ContextProvider>
<HashRouter>
<Switch>
<Route path="/login" component={AuthPage} />
<Route path="/" component={Layout} />
</Switch>
</HashRouter>
</ContextProvider>,
document.getElementById('root')
);
ContextProvider.jsx
import React from 'react';
import PropTypes from 'prop-types';
export default class ContextProvider extends React.Component {
static childContextTypes = {
user: PropTypes.object
}
// called every time the state changes
getChildContext() {
return { user: this.state.user };
}
render() {
return(
<div>
{ this.props.children }
</div>
);
}
}
AuthPage.jsx
import React from 'react';
import PropTypes from 'prop-types';
import AuthForm from './AuthForm.jsx';
import RegisterForm from './RegisterForm.jsx';
import Api from '../api.js';
export default class AuthPage extends React.Component {
static contextTypes = {
user: PropTypes.object
}
constructor(props) {
super(props);
this.updateUserContext = this.updateUserContext.bind(this);
}
updateUserContext(user) {
console.log("Updating user context");
this.context.user = user;
console.log(this.context.user);
}
render() {
return (
<div>
<AuthForm type="Login" onSubmit={Api.login} updateUser={this.updateUserContext} />
<AuthForm type="Register" onSubmit={Api.register} updateUser={this.updateUserContext} />
</div>
);
}
}
Layout.jsx
import React from 'react';
import Header from './Header.jsx';
import { Route, Switch } from 'react-router-dom';
import FridgePage from './FridgePage.jsx';
import StockPage from './StockPage.jsx';
export default class Layout extends React.Component {
render() {
return (
<div>
<Header />
<Switch>
<Route exact path="/stock" component={StockPage} />
<Route exact path="/" component={FridgePage} />
</Switch>
</div>
);
}
}
FridgePage.jsx (where I want to access this.context.user)
import React from 'react';
import PropTypes from 'prop-types';
import Api from '../api.js';
export default class FridgePage extends React.Component {
static contextTypes = {
user: PropTypes.object
}
constructor(props) {
super(props);
this.state = {
fridge: []
}
}
componentDidMount() {
debugger;
Api.getFridge(this.context.user.id)
.then((fridge) => {
this.setState({ "fridge": fridge });
})
.catch((err) => console.log(err));
}
render() {
return (
<div>
<h1>Fridge</h1>
{ this.state.fridge }
</div>
);
}
}
Simple state provider
auth module provides two functions:
withAuth - higher order component to provide authentication data to components that need it.
update - function for updating authentication status
How it works
The basic idea is that withAuth should add auth data to props that are being passed to a wrapped component.
It is done in three steps: take props that being passed to a component, add auth data, pass new props to the component.
let state = "initial state"
const withAuth = (Component) => (props) => {
const newProps = {...props, auth: state }
return <Component {...newProps} />
}
One piece that is missing is to rerender the component when the auth state changes. There are two ways to rerender a component: with setState() and forceUpdate(). Since withAuth doesn't need internal state, we will use forceUpdate() for rerendering.
We need to trigger a component rerender whenever there is a change in auth state. To do so, we need to store forceUpdate() function in a place that is accesible to update() function that will call it whenever auth state changes.
let state = "initial state"
// this stores forceUpdate() functions for all mounted components
// that need auth state
const rerenderFunctions = []
const withAuth = (Component) =>
class WithAuth extends React.Component {
componentDidMount() {
const rerenderComponent = this.forceUpdate.bind(this)
rerenderFunctions.push(rerenderComponent)
}
render() {
const newProps = {...props, auth: state }
return <Component {...newProps} />
}
}
const update = (newState) => {
state = newState
// rerender all wrapped components to reflect current auth state
rerenderFunctions.forEach((rerenderFunction) => rerenderFunction())
}
Last step is to add code that will remove rerender function when a component is going to be unmounted
let state = "initial state"
const rerenderFunctions = []
const unsubscribe = (rerenderFunciton) => {
// find position of rerenderFunction
const index = subscribers.findIndex(subscriber);
// remove it
subscribers.splice(index, 1);
}
const subscribe = (rerenderFunction) => {
// for convinience, subscribe returns a function to
// remove the rerendering when it is no longer needed
rerenderFunctions.push(rerenderFunction)
return () => unsubscribe(rerenderFunction)
}
const withAuth = (Component) =>
class WithAuth extends React.Component {
componentDidMount() {
const rerenderComponent = this.forceUpdate.bind(this)
this.unsubscribe = subscribe(rerenderComponent)
}
render() {
const newProps = {...props, auth: state }
return <Component {...newProps} />
}
componentWillUnmount() {
// remove rerenderComponent function
// since this component don't need to be rerendered
// any more
this.unsubscribe()
}
}
// auth.js
let state = "anonymous";
const subscribers = [];
const unsubscribe = subscriber => {
const index = subscribers.findIndex(subscriber);
~index && subscribers.splice(index, 1);
};
const subscribe = subscriber => {
subscribers.push(subscriber);
return () => unsubscribe(subscriber);
};
const withAuth = Component => {
return class WithAuth extends React.Component {
componentDidMount() {
this.unsubscribe = subscribe(this.forceUpdate.bind(this));
}
render() {
const newProps = { ...this.props, auth: state };
return <Component {...newProps} />;
}
componentWillUnmoount() {
this.unsubscribe();
}
};
};
const update = newState => {
state = newState;
subscribers.forEach(subscriber => subscriber());
};
// index.js
const SignInButton = <button onClick={() => update("user 1")}>Sign In</button>;
const SignOutButton = (
<button onClick={() => update("anonymous")}>Sign Out</button>
);
const AuthState = withAuth(({ auth }) => {
return (
<h2>
Auth state: {auth}
</h2>
);
});
const App = () =>
<div>
<AuthState />
{SignInButton}
{SignOutButton}
</div>;
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
playground: https://codesandbox.io/s/vKwyxYO0
here is what i did for my project:
// src/CurrentUserContext.js
import React from "react"
export const CurrentUserContext = React.createContext()
export const CurrentUserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = React.useState(null)
const fetchCurrentUser = async () => {
let response = await fetch("/api/users/current")
response = await response.json()
setCurrentUser(response)
}
return (
<CurrentUserContext.Provider value={{ currentUser, fetchCurrentUser }}>
{children}
</CurrentUserContext.Provider>
)
}
export const useCurrentUser = () => React.useContext(CurrentUserContext)
and then use it like this:
setting up the provider:
// ...
import { CurrentUserProvider } from "./CurrentUserContext"
// ...
const App = () => (
<CurrentUserProvider>
...
</CurrentUserProvider>
)
export default App
and using the context in components:
...
import { useCurrentUser } from "./CurrentUserContext"
const Header = () => {
const { currentUser, fetchCurrentUser } = useCurrentUser()
React.useEffect(() => fetchCurrentUser(), [])
const logout = async (e) => {
e.preventDefault()
let response = await fetchWithCsrf("/api/session", { method: "DELETE" })
fetchCurrentUser()
}
// ...
}
...
the full source code is available on github: https://github.com/dorianmarie/emojeet
and the project can be tried live at: http://emojeet.com/
You don't update the context, you update the ContextProvider's state which will re render the children and populate the context through getChildContext; in your context you can place functions that when called update the provider's state. Make sure you also create a high order component(HOC) named something like withAuthContext that would read the context and turned it into props for a child component to consume, much like withIntl from react-intl or withRouter from react-router among many others, this will make the development of your components simpler and context independent as if at some point you decide to just move to redux you won't have to deal with context just replace the HOC with connect and mapStateToProps.
I think I wouldn't use the context to achieve this.
Even if your app is simple (and I understand you don't want to use Redux), it's a good practice to separate the model from the view.
Consider implementing a very simple Flux architecture: create a store and dispatch actions every time you have to change the model (eg. storing user). Your views just have to listen for the store event and update their DOM.
https://facebook.github.io/flux/docs/in-depth-overview.html#content
Here's a boilerplate with a tiny helper to manage Flux : https://github.com/christianalfoni/flux-react-boilerplate/blob/master/package.json