Redux state is not updating after adding a blog post - javascript

Okay so I have this React app with a backend built with Node and Express. I got two endpoints, getAllPosts and createPost. getAllPosts returns an array of posts and createPosts returns an object with status and message.
So in the frontend I am fetching the data in Redux actions like so:
export const getPosts = () => async ( dispatch ) => {
try {
const config = {
headers: {
'Content-Type': 'application/json'
}
}
const { data } = await axios.get( '/api/posts', config );
dispatch({
type: GET_POSTS_SUCCESS,
payload: data
});
} catch ( error ) {
console.log( error )
}
}
export const createPost = ( postData ) => async ( dispatch ) => {
try {
const config = {
headers: {
'Content-Type': 'application/json'
}
}
const { data } = await axios.post( '/api/post', postData, config );
dispatch({
type: CREATE_POST_SUCCESS,
payload: data
});
} catch ( error ) {
console.log( error )
}
}
And the reducers look like this:
export const getPostsReducer = ( state = [], action ) => {
switch ( action.type ) {
case GET_POSTS_SUCCESS:
return {
loading: false,
posts: action.payload
};
default:
return state;
}
}
export const createPostReducer = ( state = [], action ) => {
switch ( action.type ) {
case CREATE_POST_SUCCESS:
return {
loading: false,
response: action.payload
};
default:
return state;
}
}
I can successfully render all posts in the first page load but when I create a post, the newly created one is not added right away. If I check the Redux dev tools, I don't see the updated list. But only renders or dev tools show the updated list after I reload the page. .
I am dispatching getPosts in one component like so:
useEffect( () => {
dispatch( getPosts() );
}, [ dispatch ] )
and dispatching the createPost in another component's submitHandler like this:
const submitHandler = ( e ) => {
e.preventDefault();
dispatch( createPost( { post } ) );
}
I also tried putting the submitHandler in the same component where getPosts is called so that dispatching it would trigger the useEffect and call getPosts again to get the updated list. But that doesn't work either. Besides that I also tried adding the returned list from getPosts as dependency in useEffect but if I log it then there are infinite amount gets printed on the log.
What am I doing wrong here?

The problem seems to be that React doesn't know that a new post was created. You'll need to call getPosts() after createPost() has been called.
const submitHandler = (e) => {
e.preventDefault();
dispatch(createPost({ post }));
dispatch(getPosts());
}
If you want to structure your services in a more centralized, scalable way, check out my blog post on React services: https://schneider-lukas.com/blog/react-connect-rest-api. You could just as well store the posts in the Provider component to make them available to all components in the tree, or better yet create a separate Context, Provider, and Consumer for handling posts.

I'd change the structure a bit:
in the createPost method instead of dispatching I'd just return data
export const createPost = ( postData ) => async ( dispatch ) => {
try {
const config = {
headers: {
'Content-Type': 'application/json'
}
}
const { data } = await axios.post( '/api/post', postData, config );
return data;
} catch ( error ) {
console.log( error )
}
}
and then in your code I'd
const submitHandler = ( e ) => {
e.preventDefault();
createPost({post}).then(data=>{
dispatch({type:CREATE_POST, data})
}
this should definitely work

Related

Redux action doesn't dispatch after page refresh

I have an issue with redux and probably useEffect(I am not sure where my mistake is). I am trying to get information from PokeAPI and store information in the redux state. The problem is that the information about pokemons don't include pokemon types(fire, water, etc.), to solve this I am sending requests to fetch those types from a different endpoint and I want to include these types of specific pokemon to redux state.
1-redux state without types of pokemons
2-redux state with types of pokemons
My goal is to have a state like in the second picture with types. But when I refresh the page, I only acquire the first picture(actions aren't dispatching). When I change something in my code and save it, I get types as well. I suspect that my problem is in the useEffect, but I couldn't find a solution without creating some nasty loops.
Note: Page parameter in fetchData coming from PokeAPI, it basically returns 15 pokemon for every page.(For now I am just experimenting on the first page)
This is my first question in stackoverflow, I already searched for similar questions but those were dealing with different aspects, so I decided to ask myself.
PokemonList.js --> this is where I need those types
import React, { useEffect } from 'react';
import { ListGroup, ListGroupItem } from "react-bootstrap";
import { useDispatch, useSelector } from 'react-redux';
import _ from "lodash";
import { GetPokemonList, GetSpecificPokemon } from '../redux/actions/PokemonAction';
import { Button } from 'react-bootstrap';
const PokemonList = () => {
const dispatch = useDispatch();
const pokemonList = useSelector(state => state.PokemonList);
useEffect(() => {
const fetchData = (page = 1) => {
dispatch(GetPokemonList(page));
}
fetchData();
}, [dispatch]);
useEffect(() => {
const fetchTypes = () => {
pokemonList.data.forEach(pokemon => {
dispatch(GetSpecificPokemon(pokemon.name));
});
}
fetchTypes();
}, [dispatch]);
const showData = () => {
if (!_.isEmpty(pokemonList.data)) {
return (
<div className="pokemon-list-wrapper">
{pokemonList.data.map((pokemon, index) => {
return (
<div className="pokemon-list-element" key={index}>
<ListGroup>
<ListGroupItem action href={`/pokemon/${pokemon.name}`} variant="success">{pokemon.name}
<Button style={{ float: "right" }}>Test</Button>
</ListGroupItem>
</ListGroup>
</div>
)
})}
</div>
)
}
if (pokemonList.loading) {
return <p>Loading...</p>
}
if (pokemonList.errorMessage !== "") {
return <p>{pokemonList.errorMessage}</p>
}
};
return (
<div>
{showData()}
</div>
)
};
export default PokemonList;
PokemonAction.js
import axios from "axios"
export const GetPokemonList = (page) => async (dispatch) => {
try {
dispatch({
type: "POKEMON_LIST_LOADING"
});
const perPage = 15;
const offset = (page * perPage) - perPage;
const res = await axios.get(`https://pokeapi.co/api/v2/pokemon?limit=${perPage}&offset=${offset}`);
dispatch({
type: "POKEMON_LIST_SUCCESS",
payload: res.data
});
} catch (e) {
dispatch({
type: "POKEMON_LIST_FAIL"
});
}
}
export const GetSpecificPokemon = (name) => async (dispatch) => {
try {
const res = await axios.get(`https://pokeapi.co/api/v2/pokemon/${name}`);
dispatch({
type: "SPECIFIC_POKEMON_SUCCESS",
payload: res.data
});
} catch (e) {
dispatch({
type: "SPECIFIC_POKEMON_FAIL"
});
}
}
PokemonListReducer.js
const initialState = {
data: [],
loading: false,
errorMessage: "",
count: 0
};
const PokemonListReducer = (state = initialState, action) => {
switch (action.type) {
case "POKEMON_LIST_LOADING":
return {
...state,
loading: true,
errorMessage: ""
};
case "POKEMON_LIST_FAIL":
return {
...state,
loading: false,
errorMessage: "unable to get pokemon"
};
case "POKEMON_LIST_SUCCESS":
return {
...state,
loading: false,
data: action.payload.results,
errorMessage: "",
count: action.payload.count
};
case "SPECIFIC_POKEMON_SUCCESS":
const typesMap = action.payload.types.map((type) => {
return type.type.name;
})
return {
...state,
data: state.data.map((pokemon) => pokemon.name === action.payload.name
? {...pokemon, types: typesMap}
: pokemon
),
loading: false,
errorMessage: ""
}
case "SPECIFIC_POKEMON_FAIL":
return {
...state,
loading: false,
errorMessage: "unable to get pokemon"
};
default:
return state
}
}
export default PokemonListReducer;
This is happening because your second useEffect does not wait for your first useEffect to finish and because of that the pokemon list is empty. On code change, since the state already has the pokemon list pre-filled, the second useEffect finds the list and does it's thing. You have to guarantee that the second action is caller right after the first one in order for this to work properly. One way to do this is to dispatch the GetSpecificPokemon action for each pokemon before finishing the GetPokemonList action. Something like this should work:
export const GetPokemonList = (page) => async (dispatch) => {
try {
dispatch({
type: "POKEMON_LIST_LOADING"
});
const perPage = 15;
const offset = (page * perPage) - perPage;
const res = await axios.get(`https://pokeapi.co/api/v2/pokemon?limit=${perPage}&offset=${offset}`);
dispatch({
type: "POKEMON_LIST_SUCCESS",
payload: res.data
});
res.data.result.forEach(pokemon => {
dispatch(GetSpecificPokemon(pokemon.name));
});
} catch (e) {
dispatch({
type: "POKEMON_LIST_FAIL"
});
}
}
Note that you won't be needing the second useEffect if you are doing this. You might also have to change displaying/not displaying the loader part yourself.
Another way is to add pokemonList as the second object in the useEffect's array parameter. I haven't tested it yet but this should work. For example:
useEffect(() => {
const fetchTypes = () => {
pokemonList.data.forEach(pokemon => {
dispatch(GetSpecificPokemon(pokemon.name));
});
}
fetchTypes();
}, [dispatch, pokemonList]);
This will call the useEffect whenever there is a change in pokemonList. In your implementation, useEffect is only called once since the value of dispatch never really changes after that. Adding pokemonList to the array results in the useEffect being called when there is a change in pokemonList also. Use this approach if you want the GetPokemonList action to always be separate from GetSpecificPokemon action i.e there are cases when both are not called together. If both are always called together then the first approach is cleaner.
That being said, these implementations actually result in a lot of network calls. The best way is to avoid the second call if possible (change your UI accordingly?) since you do not have any control over the API. If you do have control over the API you could include the extra data in the first request's response.
Edit: Here is the batch logic
const p = pokemonList.map(({ name }) =>
axios.get(`https://pokeapi.co/api/v2/pokemon/${name}`)
);
const res = await Promise.all(p);
const data = res.map((r) => ({
...r.data,
types: r.data.types.map((type) => type.type.name) // the logic you were using for types
}));
dispatch({
type: "SPECIFIC_POKEMON_SUCCESS",
payload: data
});
And then update the state in the reducer like
case "SPECIFIC_POKEMON_SUCCESS":
return {
...state,
data: action.payload,
loading: false,
errorMessage: ""
};

Passing params from one Axios request to another

Background
I'm connecting an app built in React Native to a REST API. I'm handling requests via Axios and storing the results from queries with Redux. I have an index.js file for my api connections which holds the functions that act as handlers for requests which require deeper and deeper levels of authorization. I have a simple function which returns an access token, this is triggered by the following code which currenty is located in the app's "Welcome page".
useEffect(() => {
dispatch(retrieveToken());
}, [dispatch]);
Ideally, after navigating through a couple of screens, the user would get to the Home Page and trigger the following code:
useEffect(() => {
dispatch(retrieveData());
}, [dispatch]);
So far, so good. These are the functions which dispatch triggers:
export const getToken = () =>
apiInstance
.request({
url: ENDPOINTS.TOKEN,
data: qs.stringify({
grant_type: 'some_credentials',
c_id: 'some_id',
c_secret: 'some_secret',
}),
headers: {
'content-type': 'some_content_type',
},
method: 'POST',
})
.then(response => {
return response.data;
})
.catch(error => {
return Promise.reject(error.message);
});
export const getData = () =>
apiInstance
.request({
url: ENDPOINTS.DATA,
method: 'POST',
data: qs.stringify({
timestamp: Date.now(),
c_id: 'some_id',
token: **this is the token we get from the previous function**,
}),
headers: {
'content-type': 'some_content_type',
},
})
.then(response => {
return response.data;
})
.catch(error => {
return Promise.reject(error.message);
});
Problem
As I mentioned before, this is a Redux/Axios solution. This means state is stored globally but there is an order of execution. You should note that these two functions are stored within the same file and are not called upon unless explicitly stated such as with the two dispatch calls I've showed before.
Thing is, if I log the token from Home (after calling it with dispatch) I can see it clearly, however if I try to log said token from the file which stores the request functions, I get an empty array. I've tried to fill the token field in all the following ways:
const state = store.getState()
token: state.token
const getData = (Token) =>{
...
token: Token}
And passing Token as a param within dispatch.
I've also tried daisy-chaining the different dispatches in order to force the execution of
getData after retrieving the token and not before.
Question
How can I access the result of an axios query from within another, in specific order?
It is very important to note that the data in the API can only be accessed via POST and that the error code I get when executing getData() is 401, incorrect credentials.
Also, remember this is a Redux application. The results of the queries are stored withing a global state. However this state cannot be accessed from outside components, and I cannot call it from the file in which the queries are executed given the token "does not exist at that point in time."
Action code
import keyMirror from 'keymirror';
import {createAction} from 'redux-actions';
import {getToken} from '../../api';
export const tokenActionTypes = keyMirror({
RETRIEVE_TOKEN_REQUEST: null,
RETRIEVE_TOKEN_SUCCESS: null,
RETRIEVE_TOKEN_FAILURE: null,
});
const tokenActionCreators = {
request: createAction(tokenActionTypes.RETRIEVE_TOKEN_REQUEST),
success: createAction(tokenActionTypes.RETRIEVE_TOKEN_SUCCESS),
failure: createAction(tokenActionTypes.RETRIEVE_TOKEN_FAILURE),
};
export const retrieveToken = () => dispatch => {
dispatch(tokenActionCreators.request());
getToken()
.then(token => dispatch(tokenActionCreators.success(token)))
.catch(error => dispatch(tokenActionCreators.failure(error)));
};
Reducer code
import {tokenActionTypes} from '../actions/token';
export const initialState = {
loadingToken: false,
token: [],
error: null,
};
const actionsMap = {
[tokenActionTypes.RETRIEVE_TOKEN_REQUEST]: state => ({
...state,
loadingToken: true,
}),
[tokenActionTypes.RETRIEVE_TOKEN_SUCCESS]: (state, action) => ({
...state,
loadingToken: false,
token: action.payload,
}),
[tokenActionTypes.RETRIEVE_TOKEN_FAILURE]: (state, action) => ({
...state,
loadingToken: false,
error: action.payload,
}),
};
export default (state = initialState, action) => {
const actionHandler = actionsMap[action.type];
if (!actionHandler) {
return state;
}
return actionHandler(state, action);
};
You could combine one thunk in another, like combining get token in get data:
export const retrieveToken = () => (dispatch, getState) => {
//you could use getState() to see if you need to fetch the token
// const tokenResult = selectToken(getState());
// if(token && !token expired) { return Promise.resolve() }
dispatch(tokenActionCreators.request());
//return a promise so you can wait for it
return getToken()
.then(token => dispatch(tokenActionCreators.success(token)))
.catch(error => dispatch(tokenActionCreators.failure(error)));
};
//in retrieve data you can wait for the token
export const retrieveData = () => dispatch => {
dispatch(retrieveToken()).then(
()=>{
//here return getting the data
}
)
};
A possible bug in that code is that one render cycle will dispatch multiple thunks that will get the token multiple times. You can solve that by grouping the retrieveToken action with a cache that invalidates on resolve:
const invalidateOnResolveCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
resolved: (x) => cache.delete(key),
};
};
Or you can write a wrap function for all thunks that need a token:
//group retrieveToken in such a way that if it's called multiple times
// during a render cycle the token request will only be made once
//https://gist.github.com/amsterdamharu/2dde4a6f531251f3769206ee44458af7
export const needsToken =
(fn) =>
(...args) =>
(dispatch, getState) =>
dispatch(retrieveToken(...args)).then(() =>
//you could use getState to get the token and pass it to
// fn together with the other args
// for example: fn(...args.concat(selectToken(getState())))
fn(...args)
);
export const autoTokenRetrieveData = needsToken(retrieveData);
//use needsToken for any other thunk actions that need a token
Example:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
//grouping code to group your actions
//group promise returning function
const createGroup =
(cache) =>
(fn, getKey = (...x) => JSON.stringify(x)) =>
(...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//thunk action creators are not (...args)=>result but
// (...args)=>(dispatch,getState)=>result
// so here is how we group thunk actions
const createGroupedThunkAction = (thunkAction, cache) => {
const group = createGroup(cache)(
(args, dispatch, getState) =>
thunkAction.apply(null, args)(dispatch, getState)
);
return (...args) =>
(dispatch, getState) => {
return group(args, dispatch, getState);
};
};
const createInvalidateOnResolveCache = (
cache = new Map()
) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
resolved: (key) => cache.delete(key),
};
};
//function that fetches token
const uniqueToken = (
(token) => () =>
token++
)(1);
const fetchToken = () => Promise.resolve(uniqueToken());
const initialState = {
data1: [],
data2: [],
token: null,
};
//action types
const DATA_SUCCESS = 'DATA_SUCCESS';
const GOT_TOKEN = 'GOT_TOKEN';
//action creators
const dataSuccess = (data, key) => ({
type: DATA_SUCCESS,
payload: { key, data },
});
const gotToken = (token) => ({
type: GOT_TOKEN,
payload: token,
});
const reducer = (state, { type, payload }) => {
if (type === DATA_SUCCESS) {
const { data, key } = payload;
return {
...state,
[key]: data,
};
}
if (type === GOT_TOKEN) {
return {
...state,
token: {
value: payload,
created: Date.now(),
},
};
}
return state;
};
//thunk getting the data
const getData1 = (token) => (dispatch) =>
Promise.resolve().then(() =>
dispatch(
dataSuccess(
`got data 1 with token: ${token}`,
'data1'
)
)
);
const getData2 = (token) => (dispatch) =>
Promise.resolve().then(() =>
dispatch(
dataSuccess(
`got data 2 with token: ${token}`,
'data2'
)
)
);
//thunk getting the token:
const getToken = () => (dispatch) =>
fetchToken().then((token) => dispatch(gotToken(token)));
//grouped thunk getting token
const getTokenGrouped = createGroupedThunkAction(
getToken,
createInvalidateOnResolveCache()
);
const needsToken =
(fn) =>
(...args) =>
(dispatch, getState) => {
let promise;
//only fetch token if it's older than 1 second
const tokenResult = selectToken(getState());
if (
tokenResult &&
Date.now() - new Date(tokenResult.created).getTime() <
1000
) {
promise = Promise.resolve();
} else {
promise = dispatch(getTokenGrouped(...args));
}
return promise.then(() =>
dispatch(
fn(...args.concat(selectTokenValue(getState())))
)
);
};
const getData1WithToken = needsToken(getData1);
const getData2WithToken = needsToken(getData2);
//selectors
const selectData1 = (state) => state.data1;
const selectData2 = (state) => state.data2;
const selectToken = (state) => state.token;
const selectTokenValue = createSelector(
[selectToken],
//SO snippet has no optional chaining, should just return token?.value
(token) => token && token.value
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
//simple thunk middleware
({ dispatch, getState }) =>
(next) =>
(action) =>
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
const Data1 = React.memo(function Data1({ refresh }) {
const data = useSelector(selectData1);
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(getData1WithToken());
}, [dispatch, refresh]);
return <div>{data}</div>;
});
const Data2 = React.memo(function Data2({ refresh }) {
const data = useSelector(selectData2);
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(getData2WithToken());
}, [dispatch, refresh]);
return <div>{data}</div>;
});
const App = () => {
const [refresh, setRefresh] = React.useState({});
return (
<div>
{/* getting data in one render cycle many times */}
<Data1 refresh={refresh} />
<Data2 refresh={refresh} />
<Data1 refresh={refresh} />
<Data2 refresh={refresh} />
<Data1 refresh={refresh} />
<Data2 refresh={refresh} />
<button onClick={() => setRefresh({})}>
refresh
</button>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://unpkg.com/immer#7.0.5/dist/immer.umd.production.min.js"></script>
<div id="root"></div>
Explanation:
Everywhere you see const add export so export const or export default and you can import that from any other file.
The createGroupedThunkAction receives getToken thunk and returns a thunk that is stored in getTokenGrouped.
When getTokenGrouped is called multiple times during one render (Data1 and Data2 have an effect that will do so) it will share getting the token for that render and when it resolves it'll delete the cache because of the type of cache used implemented in createInvalidateOnResolveCache. So no multiple tokens will be fetched during one render no matter how many times you dispatch it during a render.
The needsToken function will receive a thunk (getData1 and getData2) and returns a thunk that will automatically get a token by dispatching getTokenGrouped if there is no current token or if the token is older than one second (my made up logic to invalidate the token). This token is stored in state and passed to getData1 and getData2 so they can use that token.
I suggest opening the redux devtools while running the example so you can see the actions being dispatched. Normally with async you would dispatch beforeFetch, afterFetch or faildFetch for async actions but for simplicity I left that out.
You could try to use createGroupedThunkAction to make a grouped getData1 and getData2 as an exercise so there is no needless fetching for these as well.

Async redux action to fetch data is causing component to reload and cause react to react max depth in reload

I am trying to create a component that allows detecting changes in the redux store. Once the shouldUpdateData flag is set in the store, the component responsible for updating should fetch the data by using an async action creator. In my case, either the error "Maximum updates have reached" occurs or the update never happens.
Depending on the dispatch function stopFetching() (turns off the shouldUpdateData flag), the error or outcome changes. If I do the dispatch inside the action creator there are endless updates. If the code is used as it is below, no update occurs.
The reason I used the useSelector() hook from 'react-redux' is to detect a change in the store for the loading attribute.
Thank you in advance.
Here is the action creator:
export function updateDataAsync(id) {
return function (dispatch) {
// dispatch(fetchDataRequest());
return fetch(`/api/user/${id}/data`, {
method: "GET",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(
(result) => {
let {projects, notes} = result;
// New data and dispatch function
dispatch(fetchDataSuccess({projects, notes}));
},
(error) => { dispatch(fetchDataFailure(error)) }
)
}
}
Here is the reducer for this action creator:
export function savedData(state = DATA_INITIAL_STATE, action) {
switch(action.type) {
case FETCH_STATES.FETCH_DATA_REQUEST:
return {
...state,
loading: true
}
case FETCH_STATES.FETCH_DATA_SUCCESS:
return {
loading: false,
data: action.data,
error: ''
}
case FETCH_STATES.FETCH_DATA_FAILURE:
return {
loading: false,
data: {},
error: action.error.message
}
default:
return state;
}
}
The React component that is doing the update:
function StoreUpdater({ update, userId, shouldUpdate, startFetch, stopFetch, children }) {
const loading = useSelector(state => state.savedData.loading);
let reqSent = useRef(false);
useEffect(()=>{
if(!reqSent && shouldUpdate) {
startFetch();
update(userId)
reqSent.context = true;
}
})
return loading ? <LoadingAnimation /> : children;
}
const mapStateToProps = (state) => {
return {
userId: state.user.id,
shouldUpdate: state.shouldUpdateData // The flag that should trigger the update
}
}
const mapDispatchToProps = (dispatch) => {
return {
stopFetch: () => { dispatch(setShouldFetchData(false)) },
update: (id) => { dispatch(updateDataAsync(id)) },
startFetch: () => dispatch(fetchDataRequest()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(StoreUpdater);
You dint pass any dependency to useEffect so it will be called on every render which is causing infinite renders
change useEffect to
useEffect(()=>{
if(!reqSent && shouldUpdate) {
startFetch();
update(userId)
reqSent.context = true;
}
},[])
For complete information regarding useEffect refer this link
The reference I created inside the component responsible of the updates, was causing the problem. The reference was preventing the update dispatch to occur due to the if statement being false.
mapStateToProps and mapDispatchToProps were react-redux higher order functions to connect classes components into the store. there equalants at functional components are useSelector and useDispatch. re-write your HOC redux adaption into hooks, and add [ dependency ] at useEffect usage
function StoreUpdater({ update, userId, shouldUpdate, startFetch, stopFetch, children }) {
const loading = useSelector(state => state.savedData.loading);
const userId = useSelector(state => state.user.id);
const shouldUpdate = useSelector(state => state.shouldUpdateData);
let reqSent = useRef(false);
const dispatch = useDispatch() // import from 'react-redux'
useEffect(()=>{
if(!reqSent && shouldUpdate) {
dispatch(startFetch());
dispatch(update(userId));
reqSent.context = true;
}
}, [reqSent, shouldUpdate, startFetch, dispatch, update, userId])
return loading ? <LoadingAnimation /> : children;
}
export default StoreUpdater ;

Converting functions from pure react to redux react

In pure react, I have written a function that I call in componentDidMount ():
getTasks = (userId, query, statusTask, pageNumber) => {
let check = {};
axios({
url: `/api/v1/beta/${userId}`,
method: 'GET'
})
.then(res => {
check = res.data;
if (res.data) {
this.setState({
checkRunning: res.data,
checkRunningId: res.data.id
});
this.utilizeTimes(res.data.task_id);
}
})
.catch(error => {
console.log(error);
})
.then(() => {
const params = {
sort: 'name'
};
if (query) {
params['filter[qwp]'] = query;
if (this.state.tasks[0]) {
this.setState({
selectedId: this.state.tasks[0].id,
selectedTabId: this.state.tasks[0].id
});
}
}
axios({
url: '/api/v1//tasks',
method: 'GET',
params
})
.then(res => {
if (res.status === 200 && res.data) {
this.setState({
tasks: res.data,
lengthArrayTasks: parseInt(res.headers['x-pagination-total-count'])
});
if (!check && res.data && res.data[0]) {
this.setState({
selectedTabId: res.data[0].id,
});
this.load(res.data[0].id);
}
let myArrayTasks = [];
myArrayTasks = res.data;
let findObject = myArrayTasks.find(task => task.id === this.state.runningTimerTask.id);
if (
!findObject &&
this.state.runningTimerTask &&
this.state.runningTimerTask.id &&
this.state.query === ''
) {
this.setState({
tasks: [this.state.runningTimerTask, ...myArrayTasks]
});
}
}
})
.catch(error => {
console.log(error);
});
});
};
I am trying to rewrite it to redux, but with poor results. First it makes one request / api / v1 / beta / $ {userId}, writes the answer in the variable check. check passes to the nextthen. In the next then carries out the request '/ api / v1 // tasks' Can somebody help me? I am asking for some tips. Is this somehow complicated?
So far, I've managed to create something like this:
store
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
actions
export const RUNNING_TIMER = 'RUNNING_TIMER';
export const GET_TASKS = 'GET_TASKS';
export const FETCH_FAILURE = 'FETCH_FAILURE';
export const runningTimer = (userId, query, statusTask, pageNumber) => dispatch => {
console.log(userId);
axios({
url: `/api/v1/beta/${userId}`,
method: 'GET'
})
.then(({ data }) => {
dispatch({
type: RUNNING_TIMER,
payload: data
});
})
.catch(error => {
console.log(error);
dispatch({ type: FETCH_FAILURE });
})
.then(() => {
const params = {
sort: 'name'
};
axios({
url: '/api/v1//tasks',
method: 'GET',
params
})
.then(({ data }) => {
dispatch({
type: GET_TASKS,
payload: data
});
})
.catch(error => {
console.log(error);
});
});
};
reducer
import { RUNNING_TIMER, GET_TASKS } from '../actions';
const isRunningTimer = (state = {}, action) => {
const { type, payload } = action;
switch (type) {
case RUNNING_TIMER:
return {
checkRunningTimer: payload,
checkRunningTimerId: payload && payload.id ? payload.id : null
};
break;
case GET_TASKS:
return {
tasks: payload,
lengthArrayTasks: parseInt(action.headers['x-pagination-total-count'])
};
default:
return state;
}
};
const rootReducer = combineReducers({ isRunningTimer });
export default rootReducer;
App
class App extends Component {
constructor() {
super();
this.state = {
name: 'React'
};
}
componentDidMount() {
this.props.runningTimer();
}
render() {
return (
<div>
</div>
);
}
}
const mapStateToProps = state => {
const { isRunningTimer } = state;
return {
isRunningTimer
};
};
const mapDispatchToProps = dispatch => ({
runningTimer: (userId, query, statusTask, pageNumber) => dispatch(runningTimer()),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
Number 1 Consider your state design.
I find it useful to consider what the state object would look like at a given point in time.
Here is an example of initialState used in an application of mine.
const initialState = {
grocers: null,
coords: {
latitude: 37.785,
longitude: -122.406
}
};
This is injected at the createStore.
Breaking down your application state object/properties, should assist you in making your actions simpler as well.
Number 2
Consider breaking down your actions.
My thoughts, decouple the action code, at the .then at the second .then .(Consider saving the results somewhere in a user: object)
.then(response => {
const data = response.data.user;
setUsers(data);})
.catch(error => {
console.log('There has been a problem with your fetch operation: ' + error.message);
})
function setUsers(data){
dispatch({
type: FETCH_USERS,
payload: data
});
}
This refers to the S in SOLID design principles. Single Responsibility Principle.
https://devopedia.org/solid-design-principles
Number 3
Consider this, if the 'getUser' info fetch fails.
Having the process/response separated will allow the application to be debugged more cleanly. In example, the user api failed or the getTask api failed, etc.
More resources on redux.
https://redux.js.org/introduction/learning-resources#thinking-in-redux
Extending previous answer from #Cullen, this is what I did:
Since you already have a action to GET_TODOS, just make the action creator for runningTimer to do one and only one thing - make API call to /api/v1/beta/<userId> and dispatch respective actions.
export const runningTimer = (
userId,
query,
statusTask,
pageNumber
) => dispatch => {
return axios({
url: `/api/v1/beta/${userId}`,
method: "GET"
})
.then(({ data }) => {
dispatch({
type: RUNNING_TIMER,
payload: data
});
})
.catch(error => {
console.log(error);
dispatch({ type: FETCH_FAILURE });
});
};
Update props of your app component to read store data.
...
const mapStateToProps = state => {
const { isRunningTimer, todos, todo } = state;
return {
todos,
todo,
isRunningTimer,
};
};
const mapDispatchToProps = dispatch => ({
getTodos: () => dispatch(getTodos()),
getTodo: id => dispatch(getTodo(id)),
runningTimer: (userId, query, statusTask, pageNumber) => dispatch(runningTimer(userId)),
});
...
Update the implementation of componentDidMount to dispatch isRunningTimer -
componentDidMount() {
...
// call with userId 1
this.props.runningTimer(1).then(() => {
console.log(this.props);
// additional params for getTasks
const params = {
sort: 'name'
};
// another call for getTodos with names sorted
this.props.getTodos(params);
});
...
Note: You need to update your getTodos action to take in an optional params arguments (which is initialized to empty object if not passed).
Hope this helps you.
Live sandbox for this is present here - https://stackblitz.com/edit/react-redux-more-actions
Check out React-boilerplate. Great boilerplate for react and redux. They use redux-saga and redux-hooks as well.

Awaiting Redux Action w/ React Hooks

I'm trying to handle a form submission to show a loading component when the data fetch is occuring. I'd like to display the data when it's been loaded into my Redux store.
Right now, I've set up my component to use React hooks. While the data loads into my redux store successfully, I'm not sure how to "await" the result of the action being completed. Here's a simplified version of what my component looks like:
const DataPage = (props) => {
const [isLoading, setIsLoading] = useState(false);
const [isError, setError] = useState(false);
useEffect(() => { // Reset Filters when dataSource changes...
setError(false);
setIsLoading(false);
}, [dataSource]);
const handleSubmit = (e, { dataSource }) => {
e.preventDefault();
setError(false)
setIsLoading(true);
//// IDEALLY THIS IS WHERE THE FIX WOULD GO? TURN THIS INTO ASYNC/AWAIT?
props.fetchData({ dataSource, token: localStorage.JWT_TOKEN });
};
return (
<div className="dataPage">
<form className="dataPage__filters" onSubmit={(e) => handleSubmit(e, { dataSource })}>
<DataSelector dataSource={dataSource} setDataSource={setDataSource}/>
<button className="button">
Search
</button>
</form>
{isError && <div>Something went wrong...</div>}
{ isLoading ? ( <div>...Loading </div> ) : (
<div className="dataPage__table">
<DataTable /> // This is connected to my redux-store separately through 'connect'
</div>
)}
</div>
);
};
const mapDispatchToProps = (dispatch) => ({
fetchData: ({ dataSource, token }) => dispatch(startFetchData({ dataSource, token }))
});
export default connect(null, mapDispatchToProps)(DataPage);
The relevant actions (startFetchData, and setData) are located in another file, and look like this:
export const setData = (data) => ({
type: "SET_DATA",
data
});
export const startFetchData = ({ dataSource, filter, filterTarget, token }) => {
return (dispatch) => {
axios.get(`${'http://localhost:8081'}/api/${dataSource}`, { headers: { authorization: token }})
.then((res) => {
dispatch(setData(result));
});
}
};
I'd like to be able to do this without introducing any new dependencies if possible.
A note for those using TypeScript: If you want to await a promise returned by an action using useDispatch() you may see TypeScript complaining about an unnecessary await.
In this case make sure to add the correct typing (see ThunkDispatch) to useDispatch via generics.
Also with useEffect() with async-await syntax make sure to wrap your async code in another closure because useEffect() expects a void return value and Typescript otherwise complains about you returning a Promise.
const dispatch = useDispatch<ThunkDispatch<any, any, Action>>();
useEffect(() => {
(async () => {
const myResult = await dispatch(...);
const anotherResult = await dispatch(...);
// ...
})();
});
I recommend you to use redux-thunk middleware. It's really easy and useful library to able your action to be, instead of objects, functions (including async functions). I'll give you an example:
Store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
import api from './services/api';
// Note: this API requires redux#>=3.1.0
const store = createStore(
rootReducer,
// With extra argument, in this case, my API):
applyMiddleware(thunk.withExtraArgument(api));
);
AuthDuck.js
Giving this duck (types, actions and reducers in the same file, see more here)
// ----------------------------------------------------------------
// Types
// ----------------------------------------------------------------
const Types = {
SIGN_IN_START: 'SIGN_IN_START',
SIGN_IN_SUCCESS: 'SIGN_IN_SUCCESS',
SIGN_IN_FAIL: 'SIGN_IN_FAIL'
};
// ----------------------------------------------------------------
// Actions
// ----------------------------------------------------------------
const signin = function (user) {
// LOOK HERE!
// Redux Thunk able you to return a function instead of an object.
return async function (dispatch, getState, api) {
try {
dispatch({ type: Types.SIGN_IN_START });
const token = await api.access.signin(user);
dispatch({ type: Types.SIGN_IN_SUCCESS, payload: token });
} catch (error) {
dispatch({ type: Types.SIGN_IN_FAIL, payload: error });
}
};
};
export const Actions = { signin };
// ----------------------------------------------------------------
// Reducers
// ----------------------------------------------------------------
export default function reducer(state, action) {
switch (action.type) {
case VeasyCalendarTypes.SIGN_IN_START:
return { ...state, isLoading: true };
case VeasyCalendarTypes.SIGN_IN_SUCCESS:
return { ...state, isLoading: false, token: action.payload };
case VeasyCalendarTypes.SIGN_IN_FAIL:
return { ...state, isLoading: false, error: action.payload };
default:
return state;
}
};
I hope to helped you, let me know if it worked for your case :)
Best regards

Categories

Resources