I am running a pattern like so, the assumption is that SearchResultsContainer is mounted and somewhere a searchbar sets the input.
class SearchResults {
render() {
return(
<ResultsContext.Consumer value={input}>
{input => <SearchResultsContainer input=input}
</ResultsContext.Consumer>
)
}
class SearchResultsContainer
componentDidUpdate() {
//fetch data based on new input
if (check if data is the same) {
this.setState({
data: fetchedData
})
}
}
}
this will invoke a double fetch whenever a new context value has been called, because componentDidUpdate() will fire and set the data. On a new input from the results context, it will invoke componentDidUpdate(), fetch, set data, then invoke componentDidUpdate(), and fetch, then will check if data is the same and stop the loop.
Is this the right way to be using context?
The solution I used is to transfer the context to the props through a High Order Component.
I have used this very usefull github answer https://github.com/facebook/react/issues/12397#issuecomment-374004053
The result looks Like this :
my-context.js :
import React from "react";
export const MyContext = React.createContext({ foo: 'bar' });
export const withMyContext = Element => {
return React.forwardRef((props, ref) => {
return (
<MyContext.Consumer>
{context => <Element myContext={context} {...props} ref={ref} />}
</MyContext.Consumer>
);
});
};
An other component that consumes the context :
import { withMyContext } from "./path/to/my-context";
class MyComponent extends Component {
componentDidUpdate(prevProps) {
const {myContext} = this.props
if(myContext.foo !== prevProps.myContext.foo){
this.doSomething()
}
}
}
export default withMyContext(MyComponent);
There must be a context producer somewhere :
<MyContext.Provider value={{ foo: this.state.foo }}>
<MyComponent />
</MyContext.Provider>
Here is a way to do it that doesn't require passing the context through props from a parent.
// Context.js
import { createContext } from 'react'
export const Context = createContext({ example: 'context data' })
// This helps keep track of the previous context state
export class OldContext {
constructor(context) {
this.currentContext = context
this.value = {...context}
}
update() {
this.value = {...this.currentContext}
}
isOutdated() {
return JSON.stringify(this.value) !== JSON.stringify(this.currentContext)
}
}
// ContextProvider.js
import React, { Component } from 'react'
import { Context } from './Context.js'
import { MyComponent } from './MyComponent.js'
export class ContextProvider extends Component {
render(){
return (
<MyContext.provider>
{/* No need to pass context into props */}
<MyComponent />
</MyContext.provider>
)
}
}
// MyComponent.js
import React, { Component } from 'react'
import { Context, OldContext } from './Context.js'
export class MyComponent extends Component {
static contextType = Context
componentDidMount() {
this.oldContext = new OldContext(this.context)
}
componentDidUpdate() {
// Do all checks before updating the oldContext value
if (this.context.example !== this.oldContext.value.example) {
console.log('"example" in context has changed!')
}
// Update the oldContext value if the context values have changed
if (this.oldContext.isOutdated()) {
this.oldContext.update()
}
}
render(){
return <p>{this.props.context.example}</p>
}
}
You could pass just the value that is changing separately as a prop.
<MyContext.Provider value={{ foo: this.state.foo }}>
<MyComponent propToWatch={this.state.bar}/>
</MyContext.Provider>
The extent -> props wrapper seems to a recommended by the react staff. However, they dont seem to address if its an issue to wrap context in a prop for an then consume the context directly from the child of the child, etc.
If you have many of these props you are needing to watch, especially when not just at the ends of branches for the component tree, look at Redux, its more powerful that the built in React.extent.
Related
This is child component as i can you Props here
Child Component:
import React from "react";
const PeopleList = props => {
console.log("child Props :", props.data);
const list = props.data.map(item => item.name);
return <React.Fragment>{"list"}</React.Fragment>;
};
export default PeopleList;
Main Component:
import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchPeople } from "../actions/peopleaction";
import PeopleName from "../containers/peopleName";
class Main extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
this.props.dispatch(fetchPeople());
}
render() {
const { Error, peoples } = this.props;
console.log("data", peoples);
return (
<div className="main">
{"helo"}
<PeopleName data={peoples.results} />
</div>
);
}
}
const mapStateToProps = state => {
return {
peoples: state.peoples.peoples,
error: state.peoples.error
};
};
export default connect(mapStateToProps)(Main);
If i iterate the props multi objects array i can face Map is not define issue;
I need to iterate the props.data multi objects array in child component and i get object from Redux store. once component loaded the redux store.
can you please some one help me on this.
you can find whole code below mentioned
Try this It works in your codesandbox.
{peoples.results && <PeopleName data={peoples.results} />}
I'm trying out some state management in React using the Context API; what I want to achieve is that when I reach a specific route I load data from the server, store it in the context, and display it in the page itself. This is causing an infinite loop where the request to the server is done over and over (and never stops).
I'm trying to use higher order components for the provider and consumer logic:
import React, { Component, createContext } from 'react';
import RequestStatus from '../RequestStatus';
import { getData } from '../Api';
const dataCtx = createContext({
data: [],
getData: () => {},
requestStatus: RequestStatus.INACTIVE,
});
export default dataCtx;
export function dataContextProvider(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
getData: this.getData.bind(this),
requestStatus: RequestStatus.INACTIVE,
};
}
async getData() {
this.setState({ requestStatus: RequestStatus.RUNNING });
try {
const data = await getData();
this.setState({ data, requestStatus: RequestStatus.INACTIVE });
} catch (error) {
this.setState({ requestStatus: RequestStatus.FAILED });
}
}
render() {
return (
<dataCtx.Provider value={this.state}>
<WrappedComponent {...this.props} />
</dataCtx.Provider>
);
}
};
}
export function dataContextConsumer(WrappedComponent) {
return function component(props) {
return (
<dataCtx.Consumer>
{dataContext => <WrappedComponent dataCtx={dataContext} {...props} />}
</dataCtx.Consumer>
);
};
}
the provider is the App component itself:
import React, { Fragment } from 'react';
import { dataContextProvider } from './contexts/DataContext';
import { userContextProvider } from './contexts/UserContext';
import AppRoutes from './AppRoutes';
function App() {
return (
<Fragment>
<main>
<AppRoutes />
</main>
</Fragment>
);
}
export default userContextProvider(dataContextProvider(App));
and here's the consumer that causes the loop:
import React, { Component } from 'react';
import RequestStatus from './RequestStatus';
import { dataContextConsumer } from './contexts/DataContext';
class DataList extends Component {
async componentDidMount() {
const { dataCtx: { getData } } = this.props;
await getData();
}
render() {
const { dataCtx: { data, requestStatus } } = this.props;
return (
{/* display the data here */}
);
}
}
export default dataContextConsumer(DataList);
I've tried switching away from the HOC for the consumer, but it didn't help:
import React, { Component } from 'react';
import RequestStatus from './RequestStatus';
import dataCtx from './contexts/DataContext';
class DataList extends Component {
async componentDidMount() {
const { getData } = this.context;
await getData();
}
render() {
const { data, requestStatus } = this.context;
return (
{/* display the data here */}
);
}
}
DataList.contextType = dataCtx;
export default DataList;
The DataList is only one of the pages from where I'd like to trigger a context update.
I'm guessing that the Provider is causing a re-render of the whole App, but why? Where am I going wrong, and how can I fix this?
Ok, after trying to replicate the problem in a sandbox I realized what the problem was: I was wrapping a parent component in a HOC inside a render function, like so:
<Route exact path="/datapage" component={requireLoggedInUser(Page)} />
which forced the DataList component to be destroyed + recreated every time the App re-rendered.
the request loop happens because the DataList component gets re-rendered, calling ComponentDidMount, which calls getData() after each render.
A component renders if there is a change to the props or state of the component.
getData() sets the state property requestStatus (which is why your whole app gets re-rendered) which is a prop of DataList - causing a re-render of DataList.
you should not use requestStatus as a prop of DataList as you are getting that from the context anyway.
This could be because of the fact that your provider (dataContextProvider) level function getData has the same namespace as your function that you are importing from ../Api.
And then I believe that when the following line const data = await getData(); runs within the code block below, it actually calls the providers getData function, thus causing a loop.
async getData() {
this.setState({ requestStatus: RequestStatus.RUNNING });
try {
const data = await getData();
this.setState({ data, requestStatus: RequestStatus.INACTIVE });
} catch (error) {
this.setState({ requestStatus: RequestStatus.FAILED });
}
}
I am developing a new app using the new React Context API instead of Redux, and before, with Redux, when I needed to get a list of users for example, I simply call in componentDidMount my action, but now with React Context, my actions live inside my Consumer which is inside my render function, which means that every time my render function is called, it will call my action to get my users list and that is not good because I will be doing a lot of unecessary requests.
So, how I can call only one time my action, like in componentDidMount instead of calling in render?
Just to exemplify, look at this code:
Let's suppose that I am wrapping all my Providers in one component, like this:
import React from 'react';
import UserProvider from './UserProvider';
import PostProvider from './PostProvider';
export default class Provider extends React.Component {
render(){
return(
<UserProvider>
<PostProvider>
{this.props.children}
</PostProvider>
</UserProvider>
)
}
}
Then I put this Provider component wrapping all my app, like this:
import React from 'react';
import Provider from './providers/Provider';
import { Router } from './Router';
export default class App extends React.Component {
render() {
const Component = Router();
return(
<Provider>
<Component />
</Provider>
)
}
}
Now, at my users view for example, it will be something like this:
import React from 'react';
import UserContext from '../contexts/UserContext';
export default class Users extends React.Component {
render(){
return(
<UserContext.Consumer>
{({getUsers, users}) => {
getUsers();
return(
<h1>Users</h1>
<ul>
{users.map(user) => (
<li>{user.name}</li>
)}
</ul>
)
}}
</UserContext.Consumer>
)
}
}
What I want is this:
import React from 'react';
import UserContext from '../contexts/UserContext';
export default class Users extends React.Component {
componentDidMount(){
this.props.getUsers();
}
render(){
return(
<UserContext.Consumer>
{({users}) => {
getUsers();
return(
<h1>Users</h1>
<ul>
{users.map(user) => (
<li>{user.name}</li>
)}
</ul>
)
}}
</UserContext.Consumer>
)
}
}
But ofcourse that the example above don't work because the getUsers don't live in my Users view props. What is the right way to do it if this is possible at all?
EDIT: With the introduction of react-hooks in v16.8.0, you can use context in functional components by making use of useContext hook
const Users = () => {
const contextValue = useContext(UserContext);
// rest logic here
}
EDIT: From version 16.6.0 onwards. You can make use of context in lifecycle method using this.context like
class Users extends React.Component {
componentDidMount() {
let value = this.context;
/* perform a side-effect at mount using the value of UserContext */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* render something based on the value of UserContext */
}
}
Users.contextType = UserContext; // This part is important to access context values
Prior to version 16.6.0, you could do it in the following manner
In order to use Context in your lifecyle method, you would write your component like
class Users extends React.Component {
componentDidMount(){
this.props.getUsers();
}
render(){
const { users } = this.props;
return(
<h1>Users</h1>
<ul>
{users.map(user) => (
<li>{user.name}</li>
)}
</ul>
)
}
}
export default props => ( <UserContext.Consumer>
{({users, getUsers}) => {
return <Users {...props} users={users} getUsers={getUsers} />
}}
</UserContext.Consumer>
)
Generally you would maintain one context in your App and it makes sense to package the above login in an HOC so as to reuse it. You can write it like
import UserContext from 'path/to/UserContext';
const withUserContext = Component => {
return props => {
return (
<UserContext.Consumer>
{({users, getUsers}) => {
return <Component {...props} users={users} getUsers={getUsers} />;
}}
</UserContext.Consumer>
);
};
};
and then you can use it like
export default withUserContext(User);
Ok, I found a way to do this with a limitation. With the with-context library I managed to insert all my consumer data into my component props.
But, to insert more than one consumer into the same component is complicated to do, you have to create mixed consumers with this library, which makes not elegant the code and non productive.
The link to this library: https://github.com/SunHuawei/with-context
EDIT: Actually you don't need to use the multi context api that with-context provide, in fact, you can use the simple api and make a decorator for each of your context and if you want to use more than one consumer in you component, just declare above your class as much decorators as you want!
For my part it was enough to add .bind(this) to the event. This is how my Component looks like.
// Stores File
class RootStore {
//...States, etc
}
const myRootContext = React.createContext(new RootStore())
export default myRootContext;
// In Component
class MyComp extends Component {
static contextType = myRootContext;
doSomething() {
console.log()
}
render() {
return <button onClick={this.doSomething.bind(this)}></button>
}
}
The following is working for me. This is a HOC that uses useContext and useReducer hooks. There's also a way to interact with sockets in this example.
I'm creating 2 contexts (one for dispatch and one for state). You would first need to wrap some outer component with the SampleProvider HOC. Then by using one or more of the utility functions, you can gain access to the state and/or the dispatch. The withSampleContext is nice because it passes both the dispatch and state. There are also other functions like useSampleState and useSampleDispatch that can be used within a functional component.
This approach allows you to code your React components as you always have without needing to inject any Context specific syntax.
import React, { useEffect, useReducer } from 'react';
import { Client } from '#stomp/stompjs';
import * as SockJS from 'sockjs-client';
const initialState = {
myList: [],
myObject: {}
};
export const SampleStateContext = React.createContext(initialState);
export const SampleDispatchContext = React.createContext(null);
const ACTION_TYPE = {
SET_MY_LIST: 'SET_MY_LIST',
SET_MY_OBJECT: 'SET_MY_OBJECT'
};
const sampleReducer = (state, action) => {
switch (action.type) {
case ACTION_TYPE.SET_MY_LIST:
return {
...state,
myList: action.myList
};
case ACTION_TYPE.SET_MY_OBJECT:
return {
...state,
myObject: action.myObject
};
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
};
/**
* Provider wrapper that also initializes reducer and socket communication
* #param children
* #constructor
*/
export const SampleProvider = ({ children }: any) => {
const [state, dispatch] = useReducer(sampleReducer, initialState);
useEffect(() => initializeSocket(dispatch), [initializeSocket]);
return (
<SampleStateContext.Provider value={state}>
<SampleDispatchContext.Provider value={dispatch}>{children}</SampleDispatchContext.Provider>
</SampleStateContext.Provider>
);
};
/**
* HOC function used to wrap component with both state and dispatch contexts
* #param Component
*/
export const withSampleContext = Component => {
return props => {
return (
<SampleDispatchContext.Consumer>
{dispatch => (
<SampleStateContext.Consumer>
{contexts => <Component {...props} {...contexts} dispatch={dispatch} />}
</SampleStateContext.Consumer>
)}
</SampleDispatchContext.Consumer>
);
};
};
/**
* Use this within a react functional component if you want state
*/
export const useSampleState = () => {
const context = React.useContext(SampleStateContext);
if (context === undefined) {
throw new Error('useSampleState must be used within a SampleProvider');
}
return context;
};
/**
* Use this within a react functional component if you want the dispatch
*/
export const useSampleDispatch = () => {
const context = React.useContext(SampleDispatchContext);
if (context === undefined) {
throw new Error('useSampleDispatch must be used within a SampleProvider');
}
return context;
};
/**
* Sample function that can be imported to set state via dispatch
* #param dispatch
* #param obj
*/
export const setMyObject = async (dispatch, obj) => {
dispatch({ type: ACTION_TYPE.SET_MY_OBJECT, myObject: obj });
};
/**
* Initialize socket and any subscribers
* #param dispatch
*/
const initializeSocket = dispatch => {
const client = new Client({
brokerURL: 'ws://path-to-socket:port',
debug: function (str) {
console.log(str);
},
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000
});
// Fallback code for http(s)
if (typeof WebSocket !== 'function') {
client.webSocketFactory = function () {
return new SockJS('https://path-to-socket:port');
};
}
const onMessage = msg => {
dispatch({ type: ACTION_TYPE.SET_MY_LIST, myList: JSON.parse(msg.body) });
};
client.onConnect = function (frame) {
client.subscribe('/topic/someTopic', onMessage);
};
client.onStompError = function (frame) {
console.log('Broker reported error: ' + frame.headers['message']);
console.log('Additional details: ' + frame.body);
};
client.activate();
};
You have to pass context in higher parent component to get access as a props in child.
I am developing a new app using the new React Context API instead of Redux, and before, with Redux, when I needed to get a list of users for example, I simply call in componentDidMount my action, but now with React Context, my actions live inside my Consumer which is inside my render function, which means that every time my render function is called, it will call my action to get my users list and that is not good because I will be doing a lot of unecessary requests.
So, how I can call only one time my action, like in componentDidMount instead of calling in render?
Just to exemplify, look at this code:
Let's suppose that I am wrapping all my Providers in one component, like this:
import React from 'react';
import UserProvider from './UserProvider';
import PostProvider from './PostProvider';
export default class Provider extends React.Component {
render(){
return(
<UserProvider>
<PostProvider>
{this.props.children}
</PostProvider>
</UserProvider>
)
}
}
Then I put this Provider component wrapping all my app, like this:
import React from 'react';
import Provider from './providers/Provider';
import { Router } from './Router';
export default class App extends React.Component {
render() {
const Component = Router();
return(
<Provider>
<Component />
</Provider>
)
}
}
Now, at my users view for example, it will be something like this:
import React from 'react';
import UserContext from '../contexts/UserContext';
export default class Users extends React.Component {
render(){
return(
<UserContext.Consumer>
{({getUsers, users}) => {
getUsers();
return(
<h1>Users</h1>
<ul>
{users.map(user) => (
<li>{user.name}</li>
)}
</ul>
)
}}
</UserContext.Consumer>
)
}
}
What I want is this:
import React from 'react';
import UserContext from '../contexts/UserContext';
export default class Users extends React.Component {
componentDidMount(){
this.props.getUsers();
}
render(){
return(
<UserContext.Consumer>
{({users}) => {
getUsers();
return(
<h1>Users</h1>
<ul>
{users.map(user) => (
<li>{user.name}</li>
)}
</ul>
)
}}
</UserContext.Consumer>
)
}
}
But ofcourse that the example above don't work because the getUsers don't live in my Users view props. What is the right way to do it if this is possible at all?
EDIT: With the introduction of react-hooks in v16.8.0, you can use context in functional components by making use of useContext hook
const Users = () => {
const contextValue = useContext(UserContext);
// rest logic here
}
EDIT: From version 16.6.0 onwards. You can make use of context in lifecycle method using this.context like
class Users extends React.Component {
componentDidMount() {
let value = this.context;
/* perform a side-effect at mount using the value of UserContext */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* render something based on the value of UserContext */
}
}
Users.contextType = UserContext; // This part is important to access context values
Prior to version 16.6.0, you could do it in the following manner
In order to use Context in your lifecyle method, you would write your component like
class Users extends React.Component {
componentDidMount(){
this.props.getUsers();
}
render(){
const { users } = this.props;
return(
<h1>Users</h1>
<ul>
{users.map(user) => (
<li>{user.name}</li>
)}
</ul>
)
}
}
export default props => ( <UserContext.Consumer>
{({users, getUsers}) => {
return <Users {...props} users={users} getUsers={getUsers} />
}}
</UserContext.Consumer>
)
Generally you would maintain one context in your App and it makes sense to package the above login in an HOC so as to reuse it. You can write it like
import UserContext from 'path/to/UserContext';
const withUserContext = Component => {
return props => {
return (
<UserContext.Consumer>
{({users, getUsers}) => {
return <Component {...props} users={users} getUsers={getUsers} />;
}}
</UserContext.Consumer>
);
};
};
and then you can use it like
export default withUserContext(User);
Ok, I found a way to do this with a limitation. With the with-context library I managed to insert all my consumer data into my component props.
But, to insert more than one consumer into the same component is complicated to do, you have to create mixed consumers with this library, which makes not elegant the code and non productive.
The link to this library: https://github.com/SunHuawei/with-context
EDIT: Actually you don't need to use the multi context api that with-context provide, in fact, you can use the simple api and make a decorator for each of your context and if you want to use more than one consumer in you component, just declare above your class as much decorators as you want!
For my part it was enough to add .bind(this) to the event. This is how my Component looks like.
// Stores File
class RootStore {
//...States, etc
}
const myRootContext = React.createContext(new RootStore())
export default myRootContext;
// In Component
class MyComp extends Component {
static contextType = myRootContext;
doSomething() {
console.log()
}
render() {
return <button onClick={this.doSomething.bind(this)}></button>
}
}
The following is working for me. This is a HOC that uses useContext and useReducer hooks. There's also a way to interact with sockets in this example.
I'm creating 2 contexts (one for dispatch and one for state). You would first need to wrap some outer component with the SampleProvider HOC. Then by using one or more of the utility functions, you can gain access to the state and/or the dispatch. The withSampleContext is nice because it passes both the dispatch and state. There are also other functions like useSampleState and useSampleDispatch that can be used within a functional component.
This approach allows you to code your React components as you always have without needing to inject any Context specific syntax.
import React, { useEffect, useReducer } from 'react';
import { Client } from '#stomp/stompjs';
import * as SockJS from 'sockjs-client';
const initialState = {
myList: [],
myObject: {}
};
export const SampleStateContext = React.createContext(initialState);
export const SampleDispatchContext = React.createContext(null);
const ACTION_TYPE = {
SET_MY_LIST: 'SET_MY_LIST',
SET_MY_OBJECT: 'SET_MY_OBJECT'
};
const sampleReducer = (state, action) => {
switch (action.type) {
case ACTION_TYPE.SET_MY_LIST:
return {
...state,
myList: action.myList
};
case ACTION_TYPE.SET_MY_OBJECT:
return {
...state,
myObject: action.myObject
};
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
};
/**
* Provider wrapper that also initializes reducer and socket communication
* #param children
* #constructor
*/
export const SampleProvider = ({ children }: any) => {
const [state, dispatch] = useReducer(sampleReducer, initialState);
useEffect(() => initializeSocket(dispatch), [initializeSocket]);
return (
<SampleStateContext.Provider value={state}>
<SampleDispatchContext.Provider value={dispatch}>{children}</SampleDispatchContext.Provider>
</SampleStateContext.Provider>
);
};
/**
* HOC function used to wrap component with both state and dispatch contexts
* #param Component
*/
export const withSampleContext = Component => {
return props => {
return (
<SampleDispatchContext.Consumer>
{dispatch => (
<SampleStateContext.Consumer>
{contexts => <Component {...props} {...contexts} dispatch={dispatch} />}
</SampleStateContext.Consumer>
)}
</SampleDispatchContext.Consumer>
);
};
};
/**
* Use this within a react functional component if you want state
*/
export const useSampleState = () => {
const context = React.useContext(SampleStateContext);
if (context === undefined) {
throw new Error('useSampleState must be used within a SampleProvider');
}
return context;
};
/**
* Use this within a react functional component if you want the dispatch
*/
export const useSampleDispatch = () => {
const context = React.useContext(SampleDispatchContext);
if (context === undefined) {
throw new Error('useSampleDispatch must be used within a SampleProvider');
}
return context;
};
/**
* Sample function that can be imported to set state via dispatch
* #param dispatch
* #param obj
*/
export const setMyObject = async (dispatch, obj) => {
dispatch({ type: ACTION_TYPE.SET_MY_OBJECT, myObject: obj });
};
/**
* Initialize socket and any subscribers
* #param dispatch
*/
const initializeSocket = dispatch => {
const client = new Client({
brokerURL: 'ws://path-to-socket:port',
debug: function (str) {
console.log(str);
},
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000
});
// Fallback code for http(s)
if (typeof WebSocket !== 'function') {
client.webSocketFactory = function () {
return new SockJS('https://path-to-socket:port');
};
}
const onMessage = msg => {
dispatch({ type: ACTION_TYPE.SET_MY_LIST, myList: JSON.parse(msg.body) });
};
client.onConnect = function (frame) {
client.subscribe('/topic/someTopic', onMessage);
};
client.onStompError = function (frame) {
console.log('Broker reported error: ' + frame.headers['message']);
console.log('Additional details: ' + frame.body);
};
client.activate();
};
You have to pass context in higher parent component to get access as a props in child.
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