I am loading data from a public API after my component is mounted. When the data is loaded I am passing it to the reducer, but it always fires twice. This is what I have:
function MyComponent(props) {
function reducer(data, action) {
switch (action.type) {
case 'INITIALIZE':
return action.payload;
case 'ADD_NEW':
const newData = {...data};
newData.info.push({});
return newData;
}
}
const [data, dispatch] = React.useReducer(reducer, null);
useEffect(() => {
fetch(URL)
.then(response => {
dispatch({
type: 'INITIALIZE',
payload: response
});
})
.catch(error => {
console.log(error);
});
}, []);
const addNew = () => {
dispatch({ type: 'ADD_NEW' });
}
return(
<>data ? data.info.length : 'No Data Yet'</>
);
}
As you can see the component awaits for the data to populate the reducer, which, when INITIALIZE is also called twice, but I didn't care about it until I needed to call ADD_NEW, because in that case it adds two blank objects into the array instead of only one. I wen't into the documentation for side effects, but I was unable to solve it.
What is the best way to deal with this?
Here's how I would deal with the issue.
The main reason why it was re-running the action effect was because you had the reducer in the component's function. I also went ahead and fixed several other issues.
The fetch code was a little off due to how fetch works. You have to get the data type off of the response which gives another promise instead of the data directly.
You also needed to make the rendering use {} to indicate that you were using javascript rather than text.
import React, { useReducer, useState, useEffect } from "react";
import { render } from "react-dom";
import Hello from "./Hello";
import "./style.css";
const url = `https://picsum.photos/v2/list?page=3&limit=1`;
function App(props) {
const [data, dispatch] = React.useReducer(reducer, null);
useEffect(() => {
fetch(url)
.then(async response => {
dispatch({
type: "INITIALIZE",
payload: (await response.json())
});
})
.catch(error => {
console.log(error);
});
}, []);
const addNew = () => {
dispatch({ type: "ADD_NEW" });
};
console.log("here");
return (
<>
<div>{data ? JSON.stringify(data) : "No Data Yet"}</div>
<button onClick={addNew}>Test</button>
</>
);
}
render(<App />, document.getElementById("root"));
function reducer(data, action) {
switch (action.type) {
case "INITIALIZE":
console.log(action.payload, "Initialize");
return action.payload;
case "ADD_NEW":
const newData = { ...data };
newData.info = newData.info || [];
newData.info.push({});
console.log(newData);
return newData;
}
}
Related
My question is, when the next js app refreshing/reloading, redux store state not updating. I have the below code inside the component
const Landing = () => {
const freeADS = useSelector((state) => state.ads.freeAds); //this states are working fine without page refresh
useEffect(() => {
dispatch(fetchFreeAds());
}, [])
return(
{freeADS.map((data, i) => {
//some codings.........
})}
)
}
export default Landing;
redux action call
export const fetchFreeAds = () => {
return {
type: ActionTypes.FETCH_FREE_ADS
}
}
after the rootsaga / watch saga get the request, I call the handler like below
export function* handleFreeAds() {
const { response, error } = yield call(fetchFreeAds);
if (response)
{
yield put({type:"SET_FREE_ADS", payload: response.data[0]});
}
else{
}
}
actual api call goes here
export function fetchFreeAds() {
return axios.get('http://xxxxxxxxxx')
.then(response => ({ response }))
.catch(error => ({ error }))
}
I'm getting this error at the moment. pls give some support. thanks
Thanks to #slideshowp2
Problem solved by doing this miner modification. Added freeAds:[ ] backet to the initial state.
export interface State{
freeAds: null
}
export const adReducers = (state = {freeAds:[]}, {type, payload}) => {
switch(type)
case ActionTypes.SET_FREE_ADS:
return {
...state,
freeAds: payload
};
}
in the return of my react function I want to do a response.data.map(...), but I can't, "response" is undefined because it's in my useEffect (scope problem).
So I try to create a state with useState which will contain response.data, but the problem is that my console.log always returns undefined, the default state of my state.
So I try to use prevstate because I believe the problem is that the previous state is taken into account, but apparently the syntax is not good. :
const Comments = ({ postId }) => {
// States
const [allComments, setAllComments] = useState()
useEffect(() => {
async function fetchData() {
const data = {
postId: postId,
};
const response = await POST(ENDPOINTS.GET_ALL_COMMENTS, data);
if (response.data[0]) {
setAllComments((prevState) => ({
...prevState,
response.data
}))
} else {
}
}
fetchData();
console.log(allComments)
}, []);
return (
<div>
{allComments.map(...)}
</div>
);
};
I finally try to do like this:
setAllComments ((prevState) => ({
... prevState,
response
}))
This time the syntax is good, but my console.log from allComments is still undefined ...
How do I access my response.data from my return? Should we use useState, prevstate, other?
You can't .map() over an object ({}).
If your comments will be an array, you'll need to use the array spread operator ([..., ...]):
const Comments = ({ postId }) => {
const [allComments, setAllComments] = useState([]);
useEffect(() => {
async function fetchData() {
const response = await POST(ENDPOINTS.GET_ALL_COMMENTS, {
postId,
});
const data = response.data;
if (Array.isArray(data)) {
setAllComments((prevState) => [...prevState, ...data]);
} else {
throw new Error("Oops, didn't get an array.");
}
}
fetchData();
}, [postId]);
return <div>{JSON.stringify(allComments)}</div>;
};
I'm new to react-admin and I am trying to build a custom image gallery input. it should show a modal with images (data is already fetched and stored in the redux) so the user can select one or more images (upon selection an action is dispatched to update the reducer's value) and I need these selected images ids in the transform function on <Create /> so I can add the required data before dataProvider method is called.
but I have a weird issue, that might be because of my lack of react knowledge. in the snippet below, I try to get the useReducers value and then add it to the form.
import React, { useReducer, useMemo, useEffect, useCallback } from 'react';
import { Create as Ra_create } from 'react-admin';
const ctxInitialValues = {};
const galleryCtx = React.createContext(ctxInitialValues);
const CreateWithGallery = (props) => {
const [selectedImages, dispatch] = useReducer((state, { type, payload }) => {
switch (type) {
case 'UPDATE_STATE':
return { ...payload };
case 'INIT_RECORD':
return {
...state,
[payload]: [],
};
default:
return state;
}
}, ctxInitialValues);
const updateSelection = (record, image, operation) => {
if (operation === 'add') {
let newState = {
...selectedImages,
[record]: [...selectedImages[record], image],
};
dispatch({
type: 'UPDATE_STATE',
payload: newState,
});
} else if (operation === 'remove') {
let newState = {
...selectedImages,
[record]: selectedImages[record].filter((item) => item.id !== image.id),
};
dispatch({
type: 'UPDATE_STATE',
payload: newState,
});
}
};
const transformPayload = (data) => {
let transformed = {
...data,
};
// but I get {} here
for (let record in selectedImages) {
transformed[record] = selectedImages[record].map((item) => ({
id: item.id,
}));
}
return transformed;
};
useEffect(() => {
console.log(selectedImages);
// I get fresh values here
}, [selectedImages]);
const initializeRecord = (record) => {
dispatch({
type: 'INIT_RECORD',
payload: record,
});
};
return (
<galleryCtx.Provider
value={{
selectedImages,
updateSelection,
initializeRecord,
}}
>
<Ra_create {...props} transform={transformPayload}>
{props.children}
</Ra_create>
</galleryCtx.Provider>
);
};
export { galleryCtx };
export default CreateWithGallery;
when I try to access the selectedImages values in the transform function I get {}, which is the initial state. I have tried using useCallback and useMemo to make sure the values are changed after each dispatch but it did not make any difference.
there's also a similar behavior in this question as well:
React Admin: how to pass state to transform
how can I use state in the transform function?
I ended up with setting the transform prop on the component (in custom toolbar):
const CustomToolbar = (props: any) => {
const transform = useCallback((data: any) => {
return {
...data,
files: something_from_state,
};
}, [something_from_state]);
const handleClick = () => {
};
return <Toolbar {...props}>
<SaveButton
handleSubmitWithRedirect={handleClick} transform={transform}/>
</Toolbar>
};
to fix this you can use transform prop on as explained in the react-admin docs. it is still unclear though, why we can't get state in the transform function on the or .
I have this reducer which shall return all comments on the page :
case actionTypes.GET_COMMENT:
return {
...state,
comments: action.comments
}
export const getComment = (comments : Object[]) => {
return {
type : actionTypes.GET_COMMENT,
comments
}
}
Here is how i call it in component
useEffect(() => {
const getAllCommentsOnCurrentPostFromBE = (id: Number) => {
axios.get(`http://localhost:4000/getComment/${id}`)
.then(res => {
console.log('--------res,get', res.data);
dispatch(actions.getComment(res.data))
console.log('--------posts', posts);
})
.catch(err => {
console.log('--------err', err);
})
}
getAllCommentsOnCurrentPostFromBE(grabIdFromLocation())
},[])
res.data is collection of key value pairs like this {"comment":"123"}
But it is not rendering anything,any suggestions please?
There is no dispatch() function. Downloaded data do not pass to the reducer. You have to use redux-thunk to use async functions with redux.
I recommend using actions in separate files:
export const fetchDataFromDatabase = () => async (
disapatch,
getState,
) => {
const response = await axios.get();
disapatch({
type: TYPE,
data: response.data,
});
};
Then export your component export default connect(yourProps,{fetchDataFromDatabase})(YourComponent)
In your component you can call props.fetchDataFromDatabase()
Guys i am having some trouble or quite doubtful.
am having one component and one reducer.
Reducer.js
import {
ASSET_POPUP_GET_ENDPOINT,
} from 'apiCollection';
import { performGet } from 'services/rest-service/rest-service';
export const GET_ASSETS_LIST = 'stories/GET_ASSETS_LIST';
const initialState = {
imgGroup: [],
isLoading: false,
};
const modalUploadReducer = (state = initialState, action) => {
switch (action.type) {
case GET_ASSETS_LIST: {
return {
...state,
ImageJson:action.payload.imageGroup,
};
}
case GET_ASSETS_LIST_ERROR: {
return {
...state,
isLoading:false,
};
}
default:
return state;
}
};
export const getModalClose = () => (dispatch) => {
dispatch({ type: CLOSE_MODAL });
}
export const getListActionDispactcher = () => (dispatch) => {
performGet(`${ASSET_POPUP_GET_ENDPOINT}`)
.then((response) => {
const payload = response.data;
dispatch({ type: GET_ASSETS_LIST,
payload: {
...payload,
data: payload.results,
} });
})
.catch((err) => {
dispatch({ type: GET_ASSETS_LIST_ERROR, payload: err });
throw err;
});
};
export default modalUploadReducer;
and my component look like
it do have mapStateToProps and mapDispatchToProps
and one of the function
const mapDispatchToProps = dispatch => ({
getCollection: () => dispatch(getListActionDispactcher()),
});
addDocumentClick = () =>{
this.props.getAssetsCollection();
}
and is it possible to have some setState/manipulation of response after api response got from reducer in the component
based on the response i need to do some changes in addDocumentClick.
Means something like this
addDocumentClick = () =>{
this.props.getAssetsCollection().then(...based on response;
}
The correct way for solving this is setting a global loading flag and in your componentDidUpdate() method, checking for the value to determine that the action has just succeeded. You already seem to have the isLoading flag. Just set it when the action's dispatched, and unset it after it succeeds/fails. And in componentDidUpdate():
function componentDidUpdate(prevProps) {
if (prevProps.isLoading && !this.props.isLoading) {
// do something
}
}
Of course, you need to connect() your loading flag to your component to achieve this.
If all you care about is whether the assets list has changed, you can simply check for the change of that prop in componentDidUpdate():
function componentDidUpdate(prevProps) {
if (prevProps.ImageJson !== this.props.ImageJson) {
// do something
}
}
Another solution is sending a callback to your action dispatcher, which makes your code more tightly coupled and I don't recommend, but it does work too. So, when you connect(), you can:
getCollection: (onSuccess) => dispatch(getListActionDispactcher(onSuccess)),
In your action dispatcher:
export const getListActionDispactcher = (onSuccess) => (dispatch) => {
// ...once API finished/failed
onSuccess(someData);
}
Finally, in your component:
this.props.getCollection((result) => {
console.log('succeeded!', result);
// hide modal, etc..
}
You are using redux-thunk, and calling thunk will return a promise which will resolve in whatever you return in your thunk. Therefore, all you need to do is to add return value to getListActionDispactcher
export const getListActionDispactcher = () => (dispatch) => {
// return this promise
return performGet(`${ASSET_POPUP_GET_ENDPOINT}`)
.then((response) => {
const payload = response.data;
dispatch({ type: GET_ASSETS_LIST,
payload: {
...payload,
data: payload.results,
} });
// return whatever you want from promise
return payload
})
.catch((err) => {
dispatch({ type: GET_ASSETS_LIST_ERROR, payload: err });
throw err;
});
};
.
addDocumentClick = () => {
this.props.getAssetsCollection().then(payload => console.log(payload))
}
You should, however, look for ways to avoid this pattern to have your components decoupled from actions as much as possible for the sake of modularity