What is a good, or even conventional, way to use the multiple 'useContext' hooks in one React component. Usually I am pulling the state and dispatch from the provider like so:
const { state, dispatch } = useContext(thisIsTheContext);
Of course this means that I define the state and dispatch in the context itself.
Now I've read about some people making a sort of 'rootContext' where you can pass all state trough one Provider. However, this seems a little overkill to me.
Of course I can name state amd dispatch differently, however, I think it is the convention to just use these two when making use of the useReducer hook.
Anyone that could shed some light on this?
EDIT (as requested how the App.js component looks like):
function App() {
return (
<FlightDataProvider>
<TravellerProvider>
<Component /> // component here
</TravellerProvider>
</FlightDataProvider>
);
}
I think there is no need for rootContext. What I do is I define useReducer inside the specific Context Provider. I provide state and functions for a specific context like below.
FlightDataProvider.js
import React, { useReducer, createContext } from 'react'
const flightDataReducer = (state, action) => {
switch (action.type) {
case 'SET_FLIGHT_DATA':
return {
...state,
flightData: action.payload,
}
default:
return state
}
}
export const FlightDataContext = createContext();
export const FlightDataProvider = props => {
const initialState = {
flightData: 'flightData'
}
const [state, dispatch] = useReducer(flightDataReducer, initialState)
const setFlightData = newFlightData => {
dispatch({ type: 'SET_FLIGHT_DATA', payload: newFlightData })
}
return (
<FlightDataContext.Provider
value={{
flightData: state.flightData,
setFlightData
}}>
{props.children}
</FlightDataContext.Provider>
)
}
After that, if I want to subscribe to two different context in the same component, I do like this;
SomeComponent.js
import React from 'react'
import { FlightDataContext } from '...'
import { AnotherContext } from '...'
export const SomeComponent = () => {
const {
flightData,
setFlightData
} = useContext(FlightDataContext)
const {
someValue
setSomeValue
} = useContext(AnotherContext)
return (...)
}
PS you might want to separate flightDataReducer function, move it in another js file and import in inside FlightDataProvider.js
If I understand your question, you are concerned about how to overcome name clashes when pulling in multiple contexts in one react component since in their original files they are all objects having the same property 'despatch'.
You can use an aspect of es6 destructuring to rename the diff context object properties right inside your component.
Like this:
const { user, despatch: setUser } = useContext(UserContext);
const { theme, despatch: setTheme } = useContext(ThemeContext);
const { state, despatch: setState } = useReducer(reducer);
I chose the names setUser, setTheme, and setState arbitrarily. You can use any name you like.
Related
I'm working on a React project where I'm constrained to using React Redux v5, which doesn't include useDispatch and useSelector.
Nonetheless I really would like to have these hooks (or something like them) available in my app.
Therefore, I've created a wrapper component at the top level of the app which I connect using redux's connect function.
My mapStateToProps and mapDispatchToProps then just look like this:
const mapDispatchToProps = (dispatch: DispatchType) => {
return {
dispatch,
};
};
const mapStateToProps = (state: StateType) => {
return {
state,
};
};
export default connect(mapStateToProps, mapDispatchToProps)(MainLayout);
In my wrapper component, I then pass the dispatch and the state into the value:
<DispatchContext.Provider value={{ state, dispatch }}>
{children}
</DispatchContext.Provider>
Finally, I have a hook that looks like this:
const useSelectAndDispatch = () => {
const context = useContext(DispatchContext);
if (context === null) {
throw new Error("Please use useDispatch within DispatchContext");
}
const { state, dispatch } = context;
function select(selector) {
return selector(state);
}
return { dispatch, select };
};
I then use dispatch and selector in my components via useSelectAndDispatch.
I was wondering if this is an appropriate way to go about this issue, and whether I can expect any performance problems. I am using reselect, and have a good understanding of memoization. I'm just looking for opinions, since I've heard that the redux team held off implementing useDispatch and useSelector for a long time because of performance issues.
Many thanks for any opinions!
This will cause significant peformance problems. Your mapStateToProps is using the entire state object, so every time anything changes in the state, the provider must rerender. And since the provider rerendered with a new value, so too must every component that consumes the context. In short, you will be forcing most of your app to rerender anytime anything changes.
Instead of using mapStateToProps and mapDispatchToProps, i would go back to the actual store object, and build your hooks from that. Somewhere in your app is presumably a line of code that says const store = createStore(/* some options */).
Using that store variable, you can then make some hooks. If i can assume that there's only one store in your app, then the dispatch hook is trivial:
import { store } from 'wherever the store is created'
export const useMyDispatch = () => {
return store.dispatch;
}
And the selector one would be something like this. It uses .subscribe to be notified when something in the store changes, and then it uses the selector to pluck out the part of the state that you care about. If nothing changed, then the old state and new state will pass an === check, and react skips rendering. If it has changed though, the component renders (only the component that called useMySelect plus its children, not the entire app)
export const useMySelector = (selector) => {
const [value, setValue] = useState(() => {
return selector(store.getState());
});
useEffect(() => {
const unsubscribe = store.subscribe(() => {
const newValue = selector(store.getState());
setValue(newValue);
});
return unsubscribe;
}, []);
return value;
}
I'm currently trying to fetch data from my back end using the following. I'm looking to destructure each event item to an eventIndexItem component, but events remains undefined after useEffect.
import React, { useState, useEffect } from 'react'
import EventIndexItem from './event_index_item'
import 'react-modern-calendar-datepicker/lib/DatePicker.css';
import { Calendar, utils } from 'react-modern-calendar-datepicker';
const EventIndex = ({ searchValue, fetchEvents }) => {
let today = utils().getToday()
const [ selectedDay, setSelectedDay ] = useState(today)
function dateFormatter (selectedDay) {
return selectedDay.month + " " + selectedDay.day
}
let formattedDate = dateFormatter(selectedDay)
let events
useEffect (() => {
events = fetchEvents()
}, [] )
function handleEvents () {
const filterEvents = events.filter(event => {
let title = event.title
if ( searchValue === "" || title.includes(searchValue) ) {
return true
} else {
return false
}
})
return filterEvents.map(event => (
<EventIndexItem
key={event.id}
event={event}
/>
))
}
return (
<div className='event-index-container'>
<div className='event-index-left'>
<h1 className="event-index-date"> { formattedDate } </h1>
{ handleEvents() }
</div>
<div className='event-index-right'>
<Calendar
calendarClassName="event-index-calendar"
value={selectedDay}
onChange={setSelectedDay}
colorPrimary={'#00a2c7'}
/>
</div>
</div>
)
}
export default EventIndex
Redux container looks like so.
import { connect } from 'react-redux'
import EventIndex from './event_index'
import { fetchEvents } from '../../action/event_actions'
const msp = (state, ownProps) => {
let searchValue = ownProps.searchValue
return ({
events: Object.values(state.entities.events),
searchValue,
currentUser: state.entities.users[state.session.id]
})
}
const mdp = dispatch => {
return ({
fetchEvents: () => dispatch(fetchEvents())
})
}
export default connect (msp, mdp) (EventIndex)
Action / Thunks
import * as EventAPIUtil from '../util/event_api_util';
import * as RSVPApiUtil from '../util/rsvp_api_util';
export const RECEIVE_EVENTS = "RECEIVE_EVENTS";
export const RECEIVE_EVENT = "RECEIVE_EVENT";
export const REMOVE_EVENT = "REMOVE_EVENT";
const receiveEvents = ( events ) => {
return ({
type: RECEIVE_EVENTS,
events
});
};
export const fetchEvents = () => (dispatch) => (
EventAPIUtil.fetchEvents().then(events => dispatch(receiveEvents(events)))
);
Reducers
import {
RECEIVE_EVENTS,
RECEIVE_EVENT,
REMOVE_EVENT
} from '../action/event_actions';
const eventsReducer = (state = {}, action) => {
Object.freeze(state);
let newState;
switch (action.type) {
case RECEIVE_EVENTS:
return action.events;
case RECEIVE_EVENT:
newState = Object.assign({}, state, { [action.event.id]: action.event });
return newState;
case REMOVE_EVENT:
newState = Object.assign({}, state)
delete newState[action.eventId]
return newState;
default:
return state;
}
};
export default eventsReducer;
I've previously created a class component calling this.props.fetchEvents() in componentDidMount and then invoking my handleEvents() in render with no issue. However, the variable events is undefined, but I do see events being added to redux state via redux-logger. Please advise! Also feel free to critique current code as well.
There are two approaches to this problem. You can either:
Fetch from React component (don't recommend)
To do so you will need to do some refactoring to your code
All API call functions should be wrapped in a Promise.
You'll have to add the results to the Redux state through an action dispatch
You'll use async callback function in useEffect
You'll need to wrap the fetch in a conditional to check if values already exist in the state.
Why I don't recommend:
Because you are mixing between business state and UI state. Redux helps separate this by managing the business state in Redux and the UI state in React. So always try avoiding mixing them.
Dispatch an async action from Redux (recommended)
Use Redux thunk for async actions.
dispatch the action from useEffect
make the API call from the reducer.
make your conditions in the reducer before the call.
Note that you must set conditions to make the API call and update the state or else the React component will keep re-rendering.
Why I recommend:
It separates business state from UI state.
One way for implementation of the recommended approach.
createSlice For auto-generating action creators, Reducer and selectors.
createAsyncThunk For working with API in Redux.
React Redux template Offical template, use as a setup example.
I have a React Component like shown bellow (some parts are ommited) and I'm using redux for state management. The getRecentSheets action contains an AJAX request and dispatches the response to redux which updates state.sheets.recentSheets with the response's data.
All this works as expected, but on building it throws warning about useEffect has a missing dependency: 'getRecentSheets'. But if I add getRecentSheets to useEffect's dependency array it starts to rerun indefinitely and thus freezes the app.
I've read React documentation about the useEffect hook https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies but it doesn't provide a good example for such usecase. I suppose it is something with useCallback or react-redux useDispatch, but without examples I'm not sure how to implement it.
Can someone please tell me what the most concise and idiomatic way to use redux action in useEffect would be and how to avoid warnings about missing dependencies?
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import SheetList from '../components/sheets/SheetList';
import { getRecentSheets } from '../store/actions/sheetsActions';
const mapStateToProps = (state) => {
return {
recentSheets: state.sheets.recentSheets,
}
}
const mapDispatchToProps = (dispatch) => {
return {
getRecentSheets: () => dispatch(getRecentSheets()),
}
}
const Home = (props) => {
const {recentSheets, getRecentSheets} = props;
useEffect(() => {
getRecentSheets();
}, [])
return <SheetList sheets={ recentSheets } />
};
export default connect(mapStateToProps, mapDispatchToProps) (Home);
After all, it seems that correct way will be as follows:
// ...
import { useDispatch } from 'react-redux';
import { getRecentSheets } from '../store/actions/sheetsActions';
const Home = props => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getRecentSheets());
}, [dispatch])
// ...
};
This way it doesn't complain about getRecentSheets missing in dependencies array. As I understood from reading React doc on hooks that's because it's not defined inside the component. Though I'm new to frontend and I hope I didn't mess something up here.
Passing an empty array in your hook tells React your hook function will not have any dependent values from either props or state.
useEffect(() => {
getRecentSheets();
}, [])
The infinite loop arises when you declare the dispatcher as a dependency on the hook. When the component is initialized, props.recentSheets hasn't been set, and will rerender once you make your AJAX call.
useEffect(() => {
getRecentSheets();
}, [getRecentSheets])
You could try something like this:
const Home = ({recentSheets}) => {
const getRecentSheetsCallback = useCallback(() => {
getRecentSheets();
})
useEffect(() => {
getRecentSheetsCallback();
}, [recentSheets]) // We only run this effect again if recentSheets changes
return <SheetList sheets={ recentSheets } />
};
No matter how many times Homes re-renders, you retain the memoized function to your dispatch call.
Alternatively, you may have encountered find similar patterns utilizing local state and then make your effect "depend" on sheets.
const [sheets, setSheets] = useState(recentSheets)
Hope this helps
I would add a check to see if recentSheets exists or not, using that as my dependency.
useEffect(() => {
if (!recentSheets) getRecentSheets();
}, [recentSheets])
I'm getting a warning message on my app and I've tried lots of things to remove it, without success. Error Message:
React Hook useEffect has a missing dependency: 'updateUserData'.
Either include it or remove the dependency array
react-hooks/exhaustive-deps
I don't want to exclude that with a comment to avoid the issue, but I want to fix it in a "best practices" way.
I want to call that updater function and get my component updated, so I can share that context in other components.
So... what i'm doing wrong? (any code review about the rest is very welcomed!)
Thanks a million!
If I add [] as the 2nd parameter of useEffect I get the warning, and removing it I get an inifinite loop.
Also adding [updateuserData] gets an infinite loop.
import React, { useState } from "react";
import UserContext from "./UserContext";
interface iProps {
children: React.ReactNode
}
const UserProvider: React.FC<iProps> = (props) => {
// practice details
const [userState, setUserState] = useState({
id'',
name: ''
});
// practice skills
const [userProfileState, setuserProfileState] = useState([]);
// user selection
const [userQuestionsState, setuserQuestionsState] = useState({});
return (
<UserContext.Provider value={{
data: {
user: userState,
userProfile: userProfileState,
questions: userQuestionsState
},
updateuserData: (id : string) => {
// call 3 services with axios in parallel
// update state of the 3 hooks
}
}}
>
{props.children}
</UserContext.Provider>
);
};
export default UserProvider;
const UserPage: React.FC<ComponentProps> = (props) => {
const {data : {user, profile, questions}, updateUserData}: any = useContext(UserContext);
useEffect(() => {
// update information
updateUserData("abcId")
}, []);
return <div>...</div>
}
The idea is the following:
I have a context
I created provider for that content
that context exposes the data and an updater function
I use that provider in a component with the useEffect hook and I get the warning
I want to keep all the logic about fetching and updating the context inside the provider, so I don't replicate it all over the other components that are needing it.
Firstly, the infinite loop is caused by the fact that your context is updating, which is causing your component to be re-rendered, which is updating your context, which is causing your component to be re-rendered. Adding the dependency should prevent this loop, but in your case it isn't because when your context updates, a brand new updateuserData is being provided, so the ref equality check detects a change and triggers an update when you don't want it to.
One solution would be to change how you create updateUserState in your UserProvider, using e.g. useCallback to pass the same function unless one of the dependencies changes:
const UserProvider: React.FC<iProps> = (props) => {
// practice details
const [userState, setUserState] = useState({
id'',
name: ''
});
// practice skills
const [userProfileState, setuserProfileState] = useState([]);
// user selection
const [userQuestionsState, setuserQuestionsState] = useState({});
const updateuserData = useCallback(id=>{
// call your services
}, [userState, userProfileState, userQuestionsState])
return (
<UserContext.Provider value={{
data: {
user: userState,
userProfile: userProfileState,
questions: userQuestionsState
},
updateuserData
}}
>
{props.children}
</UserContext.Provider>
);
};
I am trying out state management with react hooks and the context API. I have implemented a reducer pattern following some code from a todo app, but now I want to starting fetching data regularly from an API (e.g. implementing an infinite scroll), and I'm not sure now where the best place in the code is to make these async-REST-api calls.
I'm used to using a redux middleware library like redux-observable, redux-thunk, etc. for asynchronous tasks. But now that I'm not using redux, it's not clear to me what the best way is to do async updates. I suppose I could use await-promise reducers, but that doesn't feel right.
Any suggestions? (Having implemented a reducer pattern, I'm tempted to just fall back to a full redux-with-redux-obnservable implementation, though I was hoping context would slim down all that boilerplate.)
This is probably how I would implement it. I have a standard reducer. I will also create a helper functional component to help me set up the value for my context provider.
I also made some comments in the source code. I hope the following code snippet is simple enough to follow.
import React, { useReducer, useEffect, createContext } from 'react';
import FetchService from './util/FetchService'; // some helper functions
const OrderInfoContext = createContext();
const reducer = (state, action) => {
switch (action.type) {
case 'init':
return {};
case 'changeData':
return action.payload;
default:
return state;
}
};
const changeData = data => ({
type: 'changeData',
payload: data
});
/**
* This is a helper component that generate the Provider wrapper
*/
function OrderInfoProvider(props) {
// We will persist API payload in the state so we can assign it to the Context
const [orders, dispatch] = useReducer(reducer, {});
// We use useEffect to make API calls.
useEffect(() => {
async function fetchData() {
/**
* This is just a helper to fetch data from endpoints. It can be done using
* axios or similar libraries
*/
const orders = await FetchService
.get('/api/orders');
dispatch(changeData(orders))
}
fetchData();
}, []);
/**
* we create a global object that is available to every child components
*/
return <OrderInfoContext.Provider value={[orders, dispatch]} {...props} />;
}
// Helper function to get Context
function useOrderInfo() {
const context = useContext(OrderInfoContext);
if (!context) {
throw new Error('useOrderInfo must be used within a OrderInfoProvider');
}
return context;
}
export { OrderInfoProvider, useOrderInfo , changeData };
Here is an example that uses context and useReducer hook to set an app state and a context provider for state and dispatch.
The container uses useContext to get the state and the dispatch function, useEffect to do side effects like you'd use thunk, saga or middleware if you were using redux, useMemo to map state to props and useCallback to map each auto dispatched action to props (I assume you are familiar with react redux connect.
import React, {
useEffect,
useContext,
useReducer,
useCallback,
useMemo,
} from 'react';
//store provider
const Store = React.createContext();
const initStoreProvider = (rootReducer, initialState) => ({
children,
}) => {
const [state, dispatch] = useReducer(
rootReducer,
initialState
);
return (
<Store.Provider value={{ state, dispatch }}>
{children}
</Store.Provider>
);
};
//container for component
const ComponentContainer = ({ id }) => {
const { state, dispatch } = useContext(Store);
const num = state.find((n, index) => index === id);
//side effects, asynchonously add another one if num%5===0
//this is your redux thunk
const addAsync = num % 5 === 0;
useEffect(() => {
if (addAsync)
Promise.resolve().then(dispatch({ type: 'add', id }));
}, [addAsync, dispatch, id]);
//use callback so function does not needlessly change and would
//trigger render in Component. This is mapDispatch but only for
//one function, if you have more than one then use
//useCallback for each one
const add = useCallback(
() => dispatch({ type: 'add', id }),
[dispatch, id]
);
//This is your memoized mapStateToProps
const props = useMemo(() => ({ counter: num, id }), [
num,
id,
]);
return (
<Component add={add} doNothing={dispatch} {...props} />
);
};
//use React.memo(Component) to avoid unnecessary renders
const Component = React.memo(
({ id, add, doNothing, counter }) =>
console.log('render in component', id) || (
<div>
<button onClick={add}>{counter}</button>
<button onClick={doNothing}>do nothing</button>
</div>
)
);
//initialize the store provider with root reducer and initial state
const StoreProvider = initStoreProvider(
(state, action) =>
action.type === 'add'
? state.map((n, index) =>
index === action.id ? n + 1 : n
)
: state,
[1, 8]
);
//using the store provider
export default () => (
<StoreProvider>
<ComponentContainer id={0} />
<ComponentContainer id={1} />
</StoreProvider>
);
Example is here
https://resthooks.io/ uses the flux pattern just like you want, which allows things like middlwares, debuggability, etc. However, instead of having to write thousands of lines of state management, you just need a simple declarative data definition.
const getTodo = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
});
function TodoDetail({ id }: { id: number }) {
const todo = useSuspense(getTodo, { id });
return <div>{todo.title}</div>;
}