Best way to fetch data from a REST api using react hooks and context for state management? - reactjs

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>;
}

Related

Update single element of the array in redux state

I have a redux state that contains an array of objects, for each of these object I call an api to get more data
objects.forEach((obj, index) => {
let newObj = { ...obj };
service.getMoreData()
.then(result => {
newObj.data = result;
let newObjects = [...this.props.objectsList] ;
let index = newObjects.findIndex(el => el.id === newObj.id);
if (index != -1) {
newObjects[index] = newObj;
this.props.updateMyState({ objectsList: newObjects });
}
})
When I get two very close responses the state is not updated correctly, I lose the data of the first response.
What is the right way to update a single element of the array? Thanks!
So since i don't know what service is and there isn't that much here to go off, here is what I would do from my understanding of what it looks like your doing:
So first let's set up a reducer to handle the part of redux state that you want to modify:
// going to give the reducer a default state
// array just because I don't know
// the full use case
// you have an id in your example so this is the best I can do :(
const defaultState = [{ id: 123456 }, { id: 123457 }];
const someReducer = (state=defaultState, action) => {
switch (action.type) {
// this is the main thing we're gonna use
case 'UPDATE_REDUX_ARRAY':
return [
...action.data
]
// return a default state == the state argument
default:
return [
...state
]
}
}
export default someReducer;
Next you should set up some actions for the reducer, this is optional and you can do it all inline in your component but I'd personally do it this way:
// pass data to the reducer using an action
const updateReduxArray = data => {
return {
type: 'UPDATE_REDUX_ARRAY',
data: data
}
}
// export like this because there might
// be more actions to add later
export {
updateReduxArray
}
Then use the reducer and action with React to update / render or whatever else you want
import { useState } from 'react';
import { updateReduxArray } from 'path_to_actions_file';
import { useEffect } from 'react';
import { axios } from 'axios';
import { useSelector, useDispatch } from 'react-redux';
const SomeComponent = () => {
// set up redux dispatch
const dispatch = useDispatch();
// get your redux state
const reduxArray = useSelector(state => state.reduxArray) // your gonna have to name this however your's is named
// somewhere to store your objects (state)
const [arrayOfObjects, updateArrayOfObjects] = useState([]);
// function to get data from your API
const getData = async () => {
// I'm using axios for HTTP requests as its pretty
// easy to use
// if you use map you can just return the value of all API calls at once
const updatedData = await Promise.all(reduxArray.map(async (object, index) => {
// make the api call
const response = axios.get(`https://some_api_endpoint/${object.id}`)
.then(r => r.data)
// return the original object with the addition of the new data
return {
...response,
...object
}
}))
// once all API calls are done update the state
// you could just update redux here but this is
// a clean way of doing it incase you wanna update
// the redux state more than once
// costs more memory to do this though
updateArrayOfObjects(updatedData)
}
// basicity the same as component did mount
// if you're using classes
useEffect(() => {
// get some data from the api
getData()
}, [ ])
// every time arrayOfObjects is updated
// also update redux
useEffect(() => {
// dispatch your action to the reducer
dispatch(updateReduxArray(arrayOfObjects))
}, [arrayOfObjects])
// render something to the page??
return (
<div>
{ reduxArray.length > 0
? reduxArray.map(object => <p>I am { object.id }</p>)
: <p>nothing to see here</p>
}
</div>
)
}
export default SomeComponent;
You could also do this so that you only update one object in redux at a time but even then you'd still be better off just passing the whole array to redux so I'd do the math on the component side rather than the reducer .
Note that in the component I used react state and useEffect. You might not need to do this, you could just handle it all in one place when the component mounts but we're using React so I just showcased it incase you want to use it somewhere else :)
Also lastly I'm using react-redux here so if you don't have that set up (you should do) please go away and do that first, adding your Provider to the root component. There are plenty of guides on this.

Fetching Data using React Hooks and Redux

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.

Custom hook to get data from store, if no then dispatch action and return data

I'm trying out React hooks API. To be specific, trying out a custom hook. The scenario is that I have an API for contact-types which is being used in multiple components. In all those components I have to check if the contact-types exists in the store. And if not, then dispatch an action. To avoid checks, and I wrote a hook that does the same. It checks the store if data is present, if yes, then returns it. If not, then dispatches an action to fetch it and when it's updated in the store, returns that value.
Here's the hook-
export const useContactTypes = () => {
const contactTypesFromStore = store.getState().contacts.types;
const [contactTypes, setContactTypes] = useState(contactTypesFromStore);
useEffect(() => {
if (!contactTypesFromStore) { //contact not in store
store.dispatch({ type: 'FETCH_CONTACT_TYPES' }); //dispatch action to fetch contact types
}
}, []);
store.subscribe(() => {
const newValues = store.getState().contacts.types;
if (contactTypesFromStore !== newValues) {
setContactTypes(newValues);
}
});
return contactTypes;
};
The requirements are met. But-
Is there any other approach for this?
Is this the best way to write a custom hook?
And should I worry about using redux subscribe like this?
There is no need to subscribe to the store or use the store directly, you can use the built in hooks from react-redux which will handle the subscription for you.
Note that the hooks are added in v7.1.0
react-redux hooks
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
export const useContactTypes = () => {
const contactTypes = useSelector(({ contacts }) => contacts.types, shallowEqual);
const disptach = useDispatch();
useEffect(() => {
if (!contactTypes) { // contact not in store
dispatch({ type: 'FETCH_CONTACT_TYPES' }); // dispatch action to fetch contact types
}
}, []);
return contactTypes;
}

using global state and local state in some components using react, redux(thunk)

I'm working on an application which has several components.
Component A: Displays nodes of objects relations between them as edges.
I load data from an api using (redux-thunk based) by calling a function in Component As componentDidMount() function in the global state/store.
Additionally I have to create the nodes based only of some fields of the objects stored in data.
Component B:
My intention is that if a node is clicked another component(detailed view of the node) is added to Component B. For this "detail view" I need other (additionall) fields of the objects.
My problem so far is that I need a local state of Component A also. The reason is that I also have to add temporary nodes which shouldn't be propagated to the global state/store.
So my questions are:
Can you give me an advice where I should select only the necessary fields of an Object in Component A and Component B?
How can I deal with the situation that I need a global state and a local state in Component B?
A solution is create a store and subscribe to changes to changes of this store.
the next code use an example with redux.
// ./globalState.js
import { useEffect, useState } from 'react';
import { createStore } from 'redux';
const store = createStore(function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
});
export const useGlogalStore = () => {
const [state, setState] = useState();
useEffect(() => {
return store.subscribe(() => setState(store.getState()));
}, [store]);
return [state, store.dispatch];
}
then in your app use only useGlogalStore() to read the state.
Example.
import { useGlogalStore } from './globalState.js';
export const App = () => {
const [state, dispatch] = useGlogalStore();
return <div>
Count: {state}
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Up</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Down</button>
</div>
}

How to dispatch Redux action from stateless component when route is loaded?

Goal: when loading a react-router route, dispatch a Redux action requesting asynchronic Saga worker to fetch data for the underlying stateless component of that route.
Problem: stateless components are mere functions and don't have lifecycle methods, such as componentDidMount, so I can't(?) dispatch Redux action from inside the function.
My question is partly related to Converting stateful React component to stateless functional component: How to implement "componentDidMount" kind of functionality? , but my goal is to merely dispatch a single Redux action requesting data to be populated to the store asynchronously (I use Saga, but I think that's irrelevant to the problem, as my goal is to merely dispatch an ordinary Redux action), after which the stateless component would re-render due to the changed data prop.
I am thinking of two approaches: either use some feature of react-router, or Redux's connect method. Is there a so-called "React-way" to accomplish my goal?
EDIT: the only solution I have come up with so far, is dispatching the action inside mapDispatchToProps, this way:
const mapStateToProps = (state, ownProps) => ({
data: state.myReducer.data // data rendered by the stateless component
});
const mapDispatchToProps = (dispatch) => {
// catched by a Saga watcher, and further delivered to a Saga worker that asynchronically fetches data to the store
dispatch({ type: myActionTypes.DATA_GET_REQUEST });
return {};
};
export default connect(mapStateToProps, mapDispatchToProps)(MyStatelessComponent);
However, this seems somehow dirty and not the correct way.
I don't know why you absolutly want a stateless component, while a stateful component with componentDidMount would do the job in a simple way.
Dispatching actions in mapDispatchToProps is very dangerous and may lead to dispatching not only on mount but whenever ownProps or store props changes. Side effects are not expected to be done in this method that should remains pure.
One easy way to keep your component stateless is to wrap it into an HOC (Higher-Order Component) that you could easily create:
MyStatelessComponent = withLifecycleDispatch(dispatch => ({
componentDidMount: function() { dispatch({ type: myActionTypes.DATA_GET_REQUEST })};
}))(MyStatelessComponent)
Note that if you use Redux connect after this HOC, you can easily access dispatch from props directly as if you don't use mapDispatchToProps, dispatch is injected.
You can then do something very simple like:
let MyStatelessComponent = ...
MyStatelessComponent = withLifecycle({
componentDidMount: () => this.props.dispatch({ type: myActionTypes.DATA_GET_REQUEST });
})(MyStatelessComponent)
export default connect(state => ({
date: state.myReducer.data
}))(MyStatelessComponent);
HOC definition:
import { createClass } from 'react';
const withLifeCycle = (spec) => (BaseComponent) => {
return createClass({
...spec,
render() {
return BaseComponent();
}
})
}
Here is a simple implementation of what you could do:
const onMount = (onMountFn) => (Component) => React.createClass({
componentDidMount() {
onMountFn(this.props);
},
render() {
return <Component {...this.props} />
}
});
let Hello = (props) => (
<div>Hello {props.name}</div>
)
Hello = onMount((mountProps) => {
alert("mounting, and props are accessible: name=" + mountProps.name)
})(Hello)
If you use connect around Hello component, they you can inject dispatch as props and use it instead of an alert message.
JsFiddle
Now days you can use the useEffect hook as such:
import React, { useEffect } from 'react';
const MyStatelessComponent: React.FC = (props) => {
useEffect(() => {
props.dispatchSomeAction();
});
return ...
}
This is the equivalent for the componentDidMount/componentWillMount life cycle methods of functional/stateless components.
For further reading on hooks: https://reactjs.org/docs/hooks-intro.html
I think I found the cleanest solution without having to use stateful components:
const onEnterAction = (store, dispatchAction) => {
return (nextState, replace) => {
store.dispatch(dispatchAction());
};
};
const myDataFetchAction = () => ({ type: DATA_GET_REQUEST });
export const Routes = (store) => (
<Route path='/' component={MyStatelessComponent} onEnter={onEnterAction(store, myDataFetchAction)}/>
);
The solution passes the store to a higher order function that is passed to the onEnter lifecycycle method.
Found the solution from https://github.com/reactjs/react-router-redux/issues/319
If you want it to be completely stateless you can dispatch an event when the route is entered using onEnter event.
<Route to='/app' Component={App} onEnter={dispatchAction} />
Now you can write you function here provided you either import dispatch in this file or somehow pass it as parameter.
function dispatchAction(nexState,replace){
//dispatch
}
But this solution I feel is even more dirty.
The other solution which I could be really efficient is using containers and calling componentDidMount in that.
import React,{Component,PropTypes} from 'react'
import {connect} from 'react-redux'
const propTypes = {
//
}
function mapStateToProps(state){
//
}
class ComponentContainer extends Component {
componentDidMount(){
//dispatch action
}
render(){
return(
<Component {...this.props}/> //your dumb/stateless component . Pass data as props
)
}
}
export default connect(mapStateToProps)(ComponentContainer)
In general, I don't think this is possible without some kind of trigger action which is dispatched when the component is mounted/rendered for the first time. You've achieved this by making mapDispatchToProps impure. I 100% agree with Sebastien that this is a bad idea. You could also move the impurity to the render function, which is even worse. The component lifecycle methods are meant for this! His HOC solution makes sense, if you don't want to have to write out the component classes.
I don't have much to add, but in case you just wanted to see the actual saga code, here's some pseudocode, given such a trigger action (untested):
// takes the request, *just a single time*, fetch data, and sets it in state
function* loadDataSaga() {
yield take(myActionTypes.DATA_GET_REQUEST)
const data = yield call(fetchData)
yield put({type: myActionTypes.SET_DATA, data})
}
function* mainSaga() {
yield fork(loadDataSaga);
... do all your other stuff
}
function myReducer(state, action) {
if (action.type === myActionTypes.SET_DATA) {
const newState = _.cloneDeep(state)
newState.whatever.data = action.data
newState.whatever.loading = false
return newState
} else if ( ... ) {
... blah blah
}
return state
}
const MyStatelessComponent = (props) => {
if (props.loading) {
return <Spinner/>
}
return <some stuff here {...props.data} />
}
const mapStateToProps = (state) => state.whatever;
const mapDispatchToProps = (dispatch) => {
// catched by a Saga watcher, and further delivered to a Saga worker that asynchronically fetches data to the store
dispatch({ type: myActionTypes.DATA_GET_REQUEST });
return {};
};
plus the boilerplate:
const sagaMiddleware = createSagaMiddleware();
export default connect(mapStateToProps, mapDispatchToProps)(MyStatelessComponent);
const store = createStore(
myReducer,
{ whatever: {loading: true, data: null} },
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(mainSaga)

Resources