redux actions are duplicated when I try to fetch data - reactjs

I am trying to create load more functionality by fetching only the necessary date i.e. the next one that needs to be added to the existing state that I have in the redux store, but I have a problem my redux actions are duplicated.
Component App.js
function App() {
const dispatch = useDispatch();
const data = useSelector(questionsData);
useEffect(() => {
const fetchQuestions = async () => {
dispatch(fetchQuestionsBegin());
try {
const { data } = await mainUrl(`/questions?last=5`);
return dispatch(fetchQuestionsSuccess(data));
} catch (err) {
return dispatch(fetchQuestionsFailure());
}
};
fetchQuestions();
}, [dispatch]);
return (
<>
<div>TEST</div>
</>
);
}
creating store
const store = configureStore({
reducer: {
questionsStore: questionsReducer,
},
});
export default store;
slice
const initialState = {
loading: false,
questions: [],
error: "",
};
const questionsSlice = createSlice({
name: "questions",
initialState,
reducers: {
fetchQuestionsBegin(state) {
return { ...state, loading: true, error: "" };
},
fetchQuestionsSuccess(state, action) {
return {
...state,
loading: false,
questions: [...state.questions, ...action.payload],
};
},
fetchQuestionsFailure(state, action) {
return { ...state, loading: false, error: action.payload };
},
},
});
export const { reducer: questionsReducer, actions } = questionsSlice;
export const {
fetchQuestionsBegin,
fetchQuestionsSuccess,
fetchQuestionsFailure,
} = actions;
redux
When I exclude <React.StrictMode> everything works fine.

Refer to link. Strict mode can cause multiple methods to invoke multiple times. Its most likely that your redux is ran twice when the component mounts for the first time. You can implement useRef to detect initial mount and then conditionally render after
const isMounted = useRef(false)
useEffect(() => {
isMounted.current = true;
}, [])
useEffect(() => {
if (isMounted.current) {
const fetchQuestions = async () => {
dispatch(fetchQuestionsBegin());
try {
const { data } = await mainUrl(`/questions?last=5`);
return dispatch(fetchQuestionsSuccess(data));
} catch (err) {
return dispatch(fetchQuestionsFailure());
}
};
fetchQuestions();
}
}, [dispatch]);

Related

Setting state in zustand persist middleware causing infinite loop

import create from 'zustand';
import createContext from 'zustand/context';
import { persist } from 'zustand/middleware';
let store;
const initialState = {
loading: false,
cart: {
cartItems: {},
invoiceData: {},
count: 0,
},
};
const zustandContext = createContext();
export const Provider = zustandContext.Provider;
// An example of how to get types
/** #type {import('zustand/index').UseStore<typeof initialState>} */
export const useStore = zustandContext.useStore;
export const initializeStore = (preloadedState = {}) => {
return create(
persist(
(set, get) => ({
...initialState,
...preloadedState,
updateCart: (cartData) => {
set({
cart: cartData,
});
},
setLoading: (val) => {
set({
loading: val,
});
},
modifyCart: (product, qty, type) => {
const cartData = get().cart;
// cart operations
set({
cart: tmpCartData,
});
},
}),
{
name: 'cartData',
}
)
);
};
export function useCreateStore(initialState) {
const [cartData, setCartData] = useState(null);
const [userCart, setCart] = useLocalStorage('cartData', {});
const { state: { cart = {} } = {} } = userCart;
if (typeof window === 'undefined') {
return () => initializeStore(initialState);
}
store = store ?? initializeStore(initialState);
useLayoutEffect(() => {
if (initialState && store) {
store.setState({
...store.getState(),
...initialState,
});
}
}, [initialState]);
useLayoutEffect(() => {
(async () => {
store.setState({
...store.getState(),
cart: { ...cart },
loading: true,
});
})();
}, []);
return () => store;
}
This code is inspired by Zustand documentation and by the NextJS and Zustand boilerplate. I need to sync this data with the browser's localstorage. However, calling the 'set' method inside modifyCart causes an infinite render. I have not found enough documentations regarding this.
How should I go about debugging such an issue?

React useEffect doesn't dispatch the redux action after page refresh

I'm trying to render the data from the following object of data which is coming from an API.
{
"code": 0,
"c": "verified",
"d": "verified",
"leaseInfo": {
"infoId": 6
},
"cpfPrice": "500.00",
"carCurrentLocation": {
"id": 1,
"carId": "df47a56a395a49b1a5d06a58cc42ffc4"
},
"n": "verified",
"p": "false",
"ownerCarInfo": {
"brand": "Ferrari",
"model": "0"
},
"serviceFeeRate": 0.10,
"depositPrice": "100.00",
"pics": [
{
"picid": 49,
"carId": "df47a56a395a49b1a5d06a58cc42ffc4"
},
],
"items": {
"itemid": 5,
"carId": "df47a56a395a49b1a5d06a58cc42ffc4"
}
}
I'm using react-redux to dispatch an action, where I will be provided with the data under a state named 'carDetails'.
However, when I try to access the data, if my component is refreshed, carDetails becomes undefined and hence gives "Cannot read property ownerCarInfo of undefined."
I'm obtaining and de-structuring the data of carDetails like this in my React component:
import React, {useEffect} from 'react';
import { useDispatch, useSelector } from 'react-redux';
const CarInfo = ({ match }) => {
const dispatch = useDispatch();
const details = useSelector((state) => state.carDetails);
const { loading, carDetails } = details;
const {pics, carCurrentLocation, items, ownerCarInfo} = carDetails;
useEffect(() => {
dispatch(getCarDetails(match.params.id));
}, [dispatch, match]);
return (
<div>
{loading ? (
<Loader></Loader>
) : (
<>
<p>{d.depositPrice}</p>
<p>{ownerCarInfo.brand}</p>
</>
)}
</div>
);
)
}
As long as the component or the React application is not refreshed, it retrieves data and displays it correctly. The carDetails becomes an empty array as soon as the page is refreshed.
This is the getCarDetails() action:
export const getCarDetails = (id) => async (dispatch, getState) => {
try {
dispatch({
type: CAR_DETAILS_REQUEST,
});
const { userLogin } = getState();
const { userInfo } = userLogin;
const config = {
headers: {
Authorization: userInfo.token,
'Content-Type': 'application/json',
},
};
const { data } = await axios.get(
`${BASE_API}/car/info/getDetails/${id}/${userInfo.bscId}`,
config
);
dispatch({
type: CAR_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: CAR_DETAILS_FAIL,
payload:
error.response && error.response.data.msg
? error.response.data.msg
: error.msg,
});
}
};
This is my reducer:
export const carsDetailsReducer = (state = { carDetails: [] }, action) => {
switch (action.type) {
case CAR_DETAILS_REQUEST:
return { loading: true };
case CAR_DETAILS_SUCCESS:
return { loading: false, carDetails: action.payload };
case CAR_DETAILS_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
This is how I declare carDetails in the redux store.
const reducer = combineReducers({
carDetails: carsDetailsReducer,
});
What is the cause for carDetails becoming undefined and the useEffect not running on page refresh?
If you are using axios your action should look like this with async function and await while you are calling API.
If you are passing API car id in the api link then pass the id in the parameters:
import axios from "axios";
export const loadData = (id) => async (dispatch) => {
dispatch({
type: "CAR_DETAILS_REQUEST",
});
const detailData = await axios.get("http:\\****/id");
dispatch({
type: "CAR_DETAILS_SUCCESS",
payload: {
success: detailData.data,
},
});
};
Reducer:
const initailState = { carDetails: [], loading: true };
export const carsDetailsReducer = (state = initailState, action) => {
switch (action.type) {
case CAR_DETAILS_REQUEST:
return { ...state,
loading: true
};
case CAR_DETAILS_SUCCESS:
return {...state,
loading: false,
carDetails: action.payload
};
case CAR_DETAILS_FAIL:
return { ...state,
loading: false,
error: action.payload };
default:
return ...state;
}
};
Your useEffect should only work when data is fetched:
import React, {useEffect} from 'react';
import { useDispatch, useSelector } from 'react-redux';
const CarInfo = ({ match }) => {
const dispatch = useDispatch();
const details = useSelector((state) => state.carDetails);
const { loading, carDetails } = details;
const {pics, carCurrentLocation, items, ownerCarInfo} = carDetails;
useEffect(() => {
dispatch(getCarDetails(id));
}, [dispatch]);
return (
<div>
{loading ? (
<Loader></Loader>
) : (
<>
<p>{d.depositPrice}</p>
<p>{ownerCarInfo.brand}</p>
</>
)}
</div>
You can also use it without a useEffect by making an onclick() function like this:
const loadDetailHandler = () => {
dispatch(getCarDetails(id));
};
return (
<div onClick={loadDetailHandler} >
</div>
If carDetails initial state is an array, then why are you destructuring object properties from it in your UI? Question for another time...
If after reloading the page the state reverts back to the initial state, an empty array is still a defined object. You need to track down what is causing your state.carDetails.carDetails to become undefined. If you examine your reducer notice that your CAR_DETAILS_REQUEST case wipes the carDetails state out and it becomes undefined. Honestly I'm surprised you aren't seeing this issue when your code runs normally without a page reload.
You need to hold on to that state. For good measure, you should always shallow copy the existing state when computing the next state object unless you've good reason to omit parts of state.
export const carsDetailsReducer = (state = { carDetails: [] }, action) => {
switch (action.type) {
case CAR_DETAILS_REQUEST:
return {
...state, // <-- shallow copy existing state
loading: true,
};
case CAR_DETAILS_SUCCESS:
return {
...state, // <-- shallow copy existing state
loading: false,
carDetails: action.payload
};
case CAR_DETAILS_FAIL:
return {
...state, // <-- shallow copy existing state
loading: false,
error: action.payload,
};
default:
return state;
}
};
for me, I think you should save the state in the
`case CAR_DETAILS_REQUEST:
return {
...state, // <-- shallow copy existing state
loading: true,
};
`
to be able to use it before o when you want to use a reducer you should each case
have the old state the reducer return the same sharp of initial state that put it you also used is loading and that not found in the initial state
so try to make the shape of the state
state={
isloading:false,
carDetails: []
}
also try each time to same the state by {...state ,is loading:true}
The problem is in CAR_DETAILS_REQUEST. You only return { loading: true }; so carDetails will be lost and become undefined.
Just update your reducer like this:
case CAR_DETAILS_REQUEST:
return { ...state, loading: true };

Why is my dispatch/console.log not firing when I call the action?

I was working on my final project for Flatiron and I came across a bug while working on my frontend. I tried many things, but always came back to this one issue. My callback function inside my dispatch is not firing. So while I may be interested to know whether my code should be refactored/fixed of bugs, the biggest problem is that I cannot run my dispatches through an action.
Here is my generic container:
import { useEffect } from "react"
import { connect } from "react-redux"
import * as actions from "../../actions/index"
import Component from "./Component"
const Container = (props) => {
useEffect(() => {
actions.menuItemsFetchRandom(8)
}, [])
const menuItemComponents = props.menuItems.menuItems.map((menuItem) => {
return (
<Component key={menuItem.key} menuItem={menuItem} />
)
})
return (
<div className="container">
{
props.menuItems.fetching
? "loading..."
: (
props.menuItems.error === ""
? menuItemComponents
: props.menuItems.error
)
}
</div>
)
}
const mapStateToProps = (state) => {
return {
menuItems: state.menuItems
}
}
export default connect(mapStateToProps)(Container)
And my actions.menuItemsFetchRandom() comes from /actions/menuItems.js:
import * as common from "./common"
import * as reducers from "../reducers/index"
const MENU_ITEMS_URL = common.API_URL + "menu_items/"
export const menuItemsFetchMany = (options) => {
return (dispatch) => {
dispatch({
type: reducers.MENU_ITEMS_FETCH_REQUEST
})
fetch(MENU_ITEMS_URL + options).then((response) => {
return response.json()
}).then((menuItems) => {
dispatch({
type: reducers.MENU_ITEMS_FETCH_SUCCESS,
payload: menuItems
})
}).catch((error) => {
dispatch({
type: reducers.MENU_ITEMS_FETCH_ERROR,
payload: error
})
})
}
}
export const menuItemsFetchRandom = (numberOfItems) => {
console.log("hi")
return (dispatch) => {
console.log("Hello")
dispatch({
type: reducers.MENU_ITEMS_FETCH_REQUEST
})
fetch(MENU_ITEMS_URL).then((response) => {
return response.json()
}).then((menuItems) => {
const length = menuItems.length
if (numberOfItems > length) {
numberOfItems = length
}
dispatch({
type: reducers.MENU_ITEMS_FETCH_SUCCESS,
payload: (() => {
const result = []
while (result.length !== length) {
const choice = menuItems[common.getRandomInt(length)]
if (result.includes(choice)) {
continue
}
result.push(choice)
}
})()
})
}).catch((error) => {
dispatch({
type: reducers.MENU_ITEMS_FETCH_ERROR,
payload: error
})
})
}
}
My /reducers/menuItems.js looks like this:
export const MENU_ITEMS_FETCH_REQUEST = "MENU_ITEMS_FETCH_REQUEST"
export const MENU_ITEMS_FETCH_SUCCESS = "MENU_ITEMS_FETCH_SUCCESS"
export const MENU_ITEMS_FETCH_ERROR = "MENU_ITEMS_FETCH_ERROR"
const initialState = {
menuItems: [],
error: "",
fetching: false
}
const menuItems = (state=initialState, action) => {
switch (action.type) {
case MENU_ITEMS_FETCH_REQUEST: {
return {
...state,
error: "",
fetching: true
}
}
case MENU_ITEMS_FETCH_SUCCESS: {
return {
...state,
menuItems: action.payload,
error: "",
fetching: false
}
}
case MENU_ITEMS_FETCH_ERROR: {
return {
...state,
error: action.payload,
fetching: false
}
}
default: {
return state
}
}
}
export default menuItems
But that doesn't seem to matter as the console.log inside the callback function in menuItemsFetchRandom() does not fire. I get the console.log("hi"), but not the console.log("Hello"). Which to me tells me its either something wrong with my code, or something wrong with redux-thunk.
You need to actually dispatch that action, not just call the action creator.
const dispatch = useDispatch();
useEffect(() => {
dispatch(actions.menuItemsFetchRandom(8))
}, [])
PS: also, there is no need to use connect in function components. Using useSelector and useDispatch is much easier and the official recommendation. Additionally, you are writing a pretty outdated style of redux that makes you write a multitude of the code that is required with modern redux. You are likely following very outdated tutorials.
Please see the official tutorials at https://redux.js.org/tutorials/index

React Hooks: useEffect, useReducer, createContext and localStorage; TypeError: Cannot read property of undefined

I am using local storage in my Map app for some persistent storage; also using React.createContext coupled with useReducer to share and update state amongst components.
State in local storage, and in the app when consoled are updating and present, i.e. hash-generated id, paths from Cloudinary for images.
But when I click on the map to add a marker i get:
TypeError: Cannot read property 'markers' of undefined
That is strange because of what I see in the console, and in local-storage.
My thinking is I have wired things incorrectly.
My UserContext component:
var initialState = {
avatar: '/static/uploads/profile-avatars/placeholder.jpg',
id: null,
isRoutingVisible: false,
removeRoutingMachine: false,
isLengthOfMarkersLessThanTwo: true,
markers: [],
currentMap: {}
};
var UserContext = React.createContext();
function UserProvider({ children }) {
function userReducer(state, { type, payload }) {
switch (type) {
case 'setUserId': {
return { ...state, ...{ id: payload.id } };
}
case 'setAvatar': {
return {
...state,
...{ avatar: payload.avatar }
};
}
case 'setIsRoutingVisible': {
return {
...state,
...{ isRoutingVisible: payload.isRoutingVisible }
};
}
case 'isLengthOfMarkersLessThanTwoFalse': {
return {
...state,
...{
isLengthOfMarkersLessThanTwo: payload.isLengthOfMarkersLessThanTwo
}
};
}
case 'addMarker': {
user.isLengthOfMarkersLessThanTwo
? {
...state,
markers: user.markers.concat(payload.marker)
}
: null;
break;
}
default: {
throw new Error(`Unhandled action type: ${type}`);
}
}
}
const [user, setUser] = useState(() => getLocalStorage('user', initialState));
var [state, dispatch] = useReducer(userReducer, user);
const [isAvatarUploading, setIsAvatarUploading] = useState(true);
useEffect(() => {
setLocalStorage('user', state);
}, [state]);
useEffect(() => {
console.log('state', state);
if (state.markers.length === 2) {
dispatch({
type: 'isLengthOfMarkersLessThanTwoFalse',
payload: { isLengthOfMarkersLessThanTwo: false }
});
}
}, [JSON.stringify(state.markers)]);
useEffect(() => {
console.log('state', state);
if (state.id) {
getUserAvatar()
.then(userAvatar => {
console.log('userAvatar ', userAvatar);
setIsAvatarUploading(false);
dispatch({
type: 'setAvatar',
payload: { avatar: userAvatar }
});
})
.catch(err => console.log('error thrown from getUserAvatar', err));
} else {
console.log('No user yet!');
}
}, [state.id]);
return (
<UserContext.Provider
value={{
userId: state.id,
userAvatar: state.avatar,
dispatch: dispatch,
isAvatarUploading: state.isAvatarUploading,
userImages: state.images,
userMarkers: state.markers,
userMap: state.currentMap,
removeRoutingMachine: state.removeRoutingMachine,
isRoutingVisibile: state.isRoutingVisible
}}
>
{children}
</UserContext.Provider>
);
}
export default UserContext;
export { UserProvider };
I thought I needed to have a custom hook, to use to pass the old state and use it to watch changes and update it.
var newUserState = initialState => {
const [state, setState] = useState(initialState);
var setter = useCallback(() => setState(state => !state), [setState]);
return [state, setter];
};
var [newUserState, setter] = newUserState(state)
Any help would be appreaciated!
In your addMarker case, you probably want to access the local state variable rather than the global user.
Also, from what I understood of this other question's answer you'd rather define your reducer outside of your component, because React will trigger some unwanted update when you redefine the reducer fucntion.
Whether any of these will solve your issue, I can't say...

React component does not re-render reactively (Redux Observable + rxjs + rx-http-requst)

I have an API endpoint that returns a list of users in an 'application/stream+json' type response. The items are separated by a new line character.
Example data can be seen here.
Component
class UserList extends Component {
componentDidMount() {
const { fetchUsers } = this.props;
fetchUsers();
}
render() {
const { isFetching = false, users = [] } = this.props;
if (isFetching) {
return <Loader message="Users are loading..." />;
}
if (!users || users.length === 0) {
return 'No users found.';
}
const children = users
.map(user => <UserListItem key={user.id} user={user} />);
return (
<div className="UserList">
<Paper>
<List>
<Subheader>Users</Subheader>
{children}
</List>
</Paper>
</div>
);
}
}
UserList.propTypes = {
users: PropTypes.arrayOf(PropTypes.any),
isFetching: PropTypes.bool.isRequired,
fetchUsers: PropTypes.func.isRequired,
};
UserList.defaultProps = {
users: [],
};
function mapStateToProps(state) {
const { users, isFetching } = state.users;
return {
users,
isFetching,
};
}
function mapDispatchToProps(dispatch) {
return {
fetchUsers: bindActionCreators(actions.fetchUsers, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(UserList);
Reducer
const initialState = {
users: [],
isFetching: false,
};
function fetchUsers(state) {
return {
...state,
isFetching: true,
};
}
function fetchUsersItemReceived(state, action) {
const { user } = action;
return {
...state,
users: [...state.users, user],
isFetching: false,
};
}
export default function (state = initialState, action) {
switch (action.type) {
case actionTypes.FETCH_USERS_REQUEST:
return fetchUsers(state);
case actionTypes.FETCH_USERS_ITEM_RECEIVED:
return fetchUsersItemReceived(state, action);
default:
return state;
}
}
Action (the parser is the Streaming JSON Parser found here)
export function fetchUsers() {
return {
type: actionTypes.FETCH_USERS_REQUEST,
};
}
function fetchUsersItemReceived(user) {
return {
type: actionTypes.FETCH_USERS_ITEM_RECEIVED,
user,
};
}
function fetchUsersSuccess() {
return {
type: actionTypes.FETCH_USERS_SUCCESS,
};
}
function fetchUsersFailure(error) {
return {
type: actionTypes.FETCH_USERS_FAILURE,
error,
};
}
function getJsonStream(url) {
const emitter = new Subject();
const req$ = RxHR
.get(url)
.flatMap(resp => resp.body)
.subscribe(
(data) => {
parser.write(data);
parser.onValue = (value) => {
if (!parser.key) {
emitter.next(value);
}
};
},
err => emitter.error(err),
() => emitter.complete(),
);
return emitter;
}
export const fetchUsersEpic = action$ =>
action$.ofType(actionTypes.FETCH_USERS_REQUEST)
.concatMap(() => getJsonStream(`${api.API_BASE_URL}/user`))
.map(user => fetchUsersItemReceived(user));
configureStore.js
const logger = createLogger();
const epicMiddleware = createEpicMiddleware(rootEpic);
const createStoreWithMiddleware = applyMiddleware(epicMiddleware, logger)(createStore);
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}
While the list component should be refreshed after EACH item is received, it is refreshed AFTER the whole list is received. Can someone point me to the blocking point in the code?
This isn't a solution to your particular issue (unless by accident lol) but I think a custom Observable is a better fit in this situation instead of a Subject. You can also hook into the Parser's error callback too.
Figured that others searching for streaming JSON with rxjs later might find this handy (untested)
function streamingJsonParse(data$) {
return new Observable((observer) => {
const parser = new Parser();
parser.onError = (err) => observer.error(err);
parser.onValue = (value) => {
if (!parser.key) {
observer.next(value);
}
};
// return the subscription so it's correctly
// unsubscribed for us
return data$
.subscribe({
next: (data) => parser.write(data),
error: (e) => observer.error(e),
complete: () => observer.complete()
});
});
}
function getJsonStream(url) {
return RxHR
.get(url)
.mergeMap(resp => streamingJsonParse(resp.body));
}
When you've had a chance to put together that jsbin let me know!
Turns out the problem was with the jsonParse lib. Switching to oboe.js
fixed it. Using the "!" node selector to select multiple root JSON elements i was able to transform the character stream to a user object stream.
Action
function getJsonStream(url) {
const emitter = new Subject();
const emitter = new Subject();
oboe(url)
.node('!', (item) => {
emitter.next(item);
})
.fail((error) => {
emitter.error(error);
});
return emitter;
}
export const fetchUsersEpic = action$ =>
action$.ofType(actionTypes.FETCH_USERS_REQUEST)
.switchMap(() => getJsonStream(`${api.API_BASE_URL}/user`))
.map(user => fetchUsersItemReceived(user));

Resources