I don't know why but I have infinite loop when fetching data in Redux operations.
I have an app with Redux and ReactJS.
This is my React component
const CustomersTable = (props) => {
useEffect( () => {
props.getAllCustomers()
}, []);
return <Table ...props.customers />
}
const mapStateToProps = (state) => ({
customers: state.customers,
})
const mapDispatchToProps = dispatch => ({
getAllCustomers: () => dispatch(getAllCustomers()),
})
export default connect(
mapStateToProps, mapDispatchToProps
)(CustomersTable);
This is getAllInvoices()
const fetchCustomers = async() => {
/**
* I fetch only documents with flag delete==false
*/
const snapshot = await firestore.collection("customers").where('deleted', '==', false).get()
let data = []
snapshot.forEach(doc => {
let d = doc.data();
d.id_db = doc.id
//...other
data.push(d)
})
return data
}
export const getAllCustomers = () =>
async (dispatch) => {
const customers = await fetchCustomers()
// I reset state becouse I wont duplicate inovices in tables
dispatch(actions.reset())
customers.map(customer => dispatch(
actions.fetch(customer)
))
}
And reducers
const customerReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case types.FETCH_CUSTOMERS:
return {
...state, list: [...state.list, action.item]
}
case types.RESET_CUSTOMERS:
return {
...state, list: []
}
default:
return state
}
}
I expect that reducers RESET_CUSTOMERS and then FETCH_CUSTOMERS done job. But it still working in loop reset->customers.
I thought that is still rendered the component in useEffect but I think that hook is writing good.
I tested other reducers which are copy-pase reducers from Customers and they work well.
EDIT 1
#godsenal, thanks for your reply:
actions.js:
import types from './types'
const fetch = item => ({
type: types.FETCH_CUSTOMERS, item
})
const reset = item => ({
type: types.RESET_CUSTOMERS, item
})
export default {
fetch,
reset
}
As regards <Table /> it is AntDesign component (https://ant.design/components/table/). Without that, it looks the same.
EDIT 2
It is incredible. I copied all files from modules (customers) and paste into contracts directory. Then I changed all variables, functions, etc from customer to contract. Now it working (only contracts), but customers infinite loop. Maybe something disturbs in outside a structure.
EDIT 3
I found in app.js that in mapStateToProps I added customers to props. After remove (because I don't need it in root component) it began works fine. I suspect that fetch method in <CustomerTable /> affect the <App /> component and it render in a loop. I discovered that component isn't still updated in a loop, but its mounts and unmounts in a loop.
But still, I don't understand one thing. In <App />, I still have in mapStateToProps dispatching invoice from a store (the same case as customers) and in this case, everything works fine.
Related
I need to develop a global search filter to search the products based on the product title.
To solve the problem I'm using redux to manage the global formal state. I created a reducer to filter the titles, however, I don't know how to get the array of API objects and play in the initialState data: []; To call the API I'm using createAPI from the redux toolkit.
Some part of code:
import { createSlice } from "#reduxjs/toolkit";
const searchSlice = createSlice({
name: "search",
initialState: {
data: [], // array of objects from API
filteredTitle: [],
isLoading: true
},
reducers: {
getData: (state, action) => {
state.data = action.payload;
},
searchByName: (state, action) => {
const filteredResult = state.data.filter((product) =>
product.title.toLowerCase().includes(action.payload.toLowerCase())
);
return {
...state,
filteredTitle:
action.payload.length > 0 ? filteredResult : [...state.data]
};
}
}
});
export const { searchByName, getData } = searchSlice.actions;
export default searchSlice.reducer;
In App.js
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { searchByName, getData } from "./features/slice";
import { useGetAllProductsQuery } from "./services/api";
export default function App() {
const dispatch = useDispatch();
const filteredTitle = useSelector((state) => state.search.filteredTitle);
const [searchTerm, setSearch] = useState("");
const { data } = useGetAllProductsQuery();
console.log("data", data);
const changeSearchTerm = (e) => {
setSearch(e.target.value);
};
useEffect(() => {
dispatch(searchByName(searchTerm));
dispatch(getData(data));
}, [searchTerm, dispatch]);
return (
<div>
<input onChange={changeSearchTerm} type="text" value={searchTerm} />
<div>
{filteredTitle.map((user) => (
<div>{user.name}</div>
))}
</div>
</div>
);
}
CodeSandbox
Redux Toolkit Query is made, so you won't interact with Redux store with thunk functions directly (that's what they say in their documentation).
You can only use transformResponse once creating the query. I haven't come across any ways, to be able to add an extra reducer to the slices that are made with RTK Query.
The first way, is to store the data that you are getting from the query, inside another slice, and then use a reducer to filter your data.
The second way is to create a slice, and using createAsyncThunk, fetch your data, then treat your data as you want it. I can leave an example for you:
export const getName = createAsyncThunk('getNameFromBE',
async name => {
await fetch('https://example.com/studentID)
}
)
export const studnetSlice = createSlice({
name: 'studentSlice',
initialState,
reducers: {
aReducer : (state, action) => {state.name = action.payload}
},
extraReducers: {
[getName .pending]: state => {
state.loading = true
state.failed = false
},
[getName .fulfilled]: (state, action) => {
state.name= action.payload
state.loading = false
},
[getName .rejected]: state => {
state.loading = false
state.rejected = true
},
}
})
Here you have a slice, with thunk and normal reducers, with a state that is accessible, but the downside is that you have to manage fetch completely manually (can be an advantage too), and also the call state (pending, fulfilled, rejected) must be done manually, which takes a lot of boilerplate code writing.
I am implementing a context that manages all the messages of a conversation.
To reduce the complexity of my algorithm, I have decided to use a Map "sectionsRef" for accessing some stuff in O(1).
This map, needs to be updated inside my reducer's logic, where I update the stateful data, in order to synchronize both.
export function MessagesProvider({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, initialState);
const sectionsRef = useMemo(() => new Map(), []);
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift));
};
const addMessage = (message) => addMessages([message]);
const deleteMessage = (messageId) => {
dispatch(actionCreators.deleteMessage(messageId));
};
const value = useMemo(() => ({
messages,
addMessages,
deleteMessage,
// eslint-disable-next-line react-hooks/exhaustive-deps
}), [messages]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
As you can see, I am using useMemo when initializing the Map in order to prevent re-initializations due to re-renders.
Is it correct to pass it as a payload to my reducer actions?
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift, sectionsRef)); <---
};
To simplify my problem, imagine this is the real code:
//
// Reducer action
//
function reducerAction(state, messages, sectionsRef, title) {
state.push(...messages);
sectionsRef.set(title, state.length - 1);
}
//
// Context code
//
const state = [];
const firstMessagesSection = [{ id: 1 }];
const secondMessagesSection = [{ id: 1 }, { id: 2 }]
const sectionsRef = new Map();
reducerAction(state, firstMessagesSection, sectionsRef, "first section");
reducerAction(state, secondMessagesSection, sectionsRef, "second section");
console.log(state);
console.log(sectionsRef.get("second section"));
I am asking this because I have read that we shouldn't run side effects inside the reducers logic... so, if I need to synchronize that map with the state, what should I do instead?
Is it correct to pass it as a payload to my reducer actions?
No: reducers must be pure functions.
Redux describes reducers using a short list which I think is very useful:
Rules of Reducers​
We said earlier that reducers must always follow some special rules:
They should only calculate the new state value based on the state and action arguments
They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
They must not do any asynchronous logic or other "side effects"
The second and third items together describe pure functions, and the first one is just a Redux-specific convention.
In your example, you are violating two rules of pure functions:
mutating state with state.push(...messages) (rather than creating a new array and returning it), and
performing side-effects by modifying a variable in the outer scope: sectionsRef.set(title, state.length - 1)
Further, you seem to never use the Map (how is it accessed in your program?). It should be included in your context, and you can simply define it outside your component (its identity will never change so it won't cause a re-render).
Here's how you can refactor your code to achieve your goal:
Keep the reducer data pure:
// store.js
export function messagesReduer (messages, action) {
switch (action.type) {
case 'ADD': {
const {payload, unshift} = action;
return unshift ? [...payload, ...messages] : [...messages, ...payload];
}
case 'DELETE': {
const {payload} = action;
return messages.filter(m => m.id !== payload);
}
}
}
export const creators = {};
creators.add = (messages, unshift = false) => ({type: 'ADD', payload: messages, unshift});
creators.delete = (id) => ({type: 'DELETE', payload: id});
export const sections = new Map();
Update the Map at the same that you dispatch an action to the related state by combining those operations in a function:
// MessagesContext.jsx
import {
createContext,
useCallback,
useMemo,
useReducer,
} from 'react';
import {
creators,
messagesReduer,
sections,
} from './store';
export const MessagesContext = createContext();
export function MessagesProvider ({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, []);
const addMessages = useCallback((title, messages, unshift = false) => {
dispatch(creators.add(messages, unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const addMessage = useCallback((title, message, unshift = false) => {
dispatch(creators.add([message], unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const deleteMessage = useCallback((id) => {
dispatch(creators.delete(id));
}, [creators.delete, dispatch]);
const value = useMemo(() => ({
addMessage,
addMessages,
deleteMessage,
messages,
sections,
}), [
addMessage,
addMessages,
deleteMessage,
messages,
sections,
]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
Use the context:
// App.jsx
import {useContext} from 'react';
import {MessagesContext, MessagesProvider} from './MessagesContext';
function Messages () {
const {
// addMessage,
// addMessages,
// deleteMessage,
messages,
// sections,
} = useContext(MessagesContext);
return (
<ul>
{
messages.map(({id}, index) => (
<li key={id}>Message no. {index + 1}: ID {id}</li>
))
}
</ul>
);
}
export function App () {
return (
<MessagesProvider>
<Messages />
</MessagesProvider>
);
}
Additional notes:
Make sure your dependency lists (e.g. in useMemo, etc.) are exhaustive. Those lint warnings are there to help prevent you from making mistakes. In general, you should never need to suppress them.
I am migrating my component from a class component to a functional component using hooks. I need to access the states with useSelector by triggering an action when the state mounts. Below is what I have thus far. What am I doing wrong? Also when I log users to the console I get the whole initial state ie { isUpdated: false, users: {}}; instead of just users
reducers.js
const initialState = {
isUpdated: false,
users: {},
};
const generateUsersObject = array => array.reduce((obj, item) => {
const { id } = item;
obj[id] = item;
return obj;
}, {});
export default (state = { ...initialState }, action) => {
switch (action.type) {
case UPDATE_USERS_LIST: {
return {
...state,
users: generateUsersObject(dataSource),
};
}
//...
default:
return state;
}
};
action.js
export const updateUsersList = () => ({
type: UPDATE_USERS_LIST,
});
the component hooks I am using
const users = useSelector(state => state.users);
const isUpdated = useSelector(state => state.isUpdated);
const dispatch = useDispatch();
useEffect(() => {
const { updateUsersList } = actions;
dispatch(updateUsersList());
}, []);
first, it will be easier to help if the index/store etc will be copied as well. (did u used thunk?)
second, your action miss "dispatch" magic word -
export const updateUsersList = () =>
return (dispatch, getState) => dispatch({
type: UPDATE_USERS_LIST
});
it is highly suggested to wrap this code with { try } syntax and be able to catch an error if happened
third, and it might help with the console.log(users) error -
there is no need in { ... } at the reducer,
state = intialState
should be enough. this line it is just for the first run of the store.
and I don't understand where { dataSource } comes from.
I'm using useEffect in combination with reduct actions. I'm aware that I have to extract the action function from the props and provide it as the second argument which generally works for bulk fetches. But if I use an ID from the props as well, it ends up in an infinity loop:
export function EmployeeEdit(props) {
const { fetchOneEmployee } = props;
const id = props.match.params.id;
const [submitErrors, setSubmitErrors] = useState([]);
useEffect(() => fetchOneEmployee(id), [fetchOneEmployee, id]);
const onSubmit = employee => {
employee = prepareValuesForSubmission(employee);
props.updateEmployee(employee._id, employee)
.then( () => props.history.goBack() )
.catch( err => setSubmitErrors(extractErrors(err.response)) );
};
return (
<div>
<h3>Edit Employee</h3>
<NewEmployee employee={props.employee} employees={props.employees} onSubmit={onSubmit} submitErrors={submitErrors} />
</div>
)
}
EmployeeEdit.propTypes = {
fetchOneEmployee: PropTypes.func.isRequired,
employees: PropTypes.array.isRequired,
employee: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
employees: state.employees.items,
employee: state.employees.item
})
export default connect(mapStateToProps, { fetchOneEmployee, updateEmployee })(EmployeeEdit);
And the redux action:
export const fetchOneEmployee = (id) => dispatch => {
axios.get(`http://localhost:8888/api/users/${id}`)
.then(res => {
const employee = res.data;
dispatch({
type: FETCH_ONE_EMPLOYEE,
payload: employee
})
})
});
};
Anybody an idea what I'm doing wrong?
one of the values in your dependency array ([fetchOneEmployee, id]) is changing. It is hard to say which value it is with the limited code you have supplied.
At first glance though, you probably want fetchOne instead of fetchOneEmployee in your array.
Your inifite loop is probably caused because of fetchOneEmployee passed as argument to useEffect. Did you pass fetchOneEmployee to EmployeeEdit as arrow function? If you did then fetchOneEmployee always will be change.
This is a part of great article about react hooks fetch data.
https://www.robinwieruch.de/react-hooks-fetch-data
I especially recommended header CUSTOM DATA FETCHING HOOK
I just started using Redux, as my application got complicated with the passing states to different component. I encountered an error while trying to retrieve data to another components.
I want to pass a state to another component, but I get this errror:
store.js
import {createStore} from 'redux';
const initialState = {
regionId: 0
};
const reducer = (state = initialState, action) => {
if(action.type === "REGIONAL_ID") {
return {
regionId: action.regionId
};
}
return state;
}
const store = createStore(reducer);
store.subscribe(() => {
console.log(store.getState().regionId)
})
export default store;
In my Network.js component im calling this
const REGION_ID = store.subscribe(() => {
return store.getState().regionId;
})
console.log(REGION_ID);
I tried the following but that gave me an error also. Many thanks!
const REGION_ID = return store.getState().regionId;
This is not an error. The store.subscribe() method returns an unsubscribe method, and the console.log() displays the implementation of this method:
const REGION_ID = store.subscribe(() => {
return store.getState().regionId; // this is isn't returned to REGION_ID
})
console.log(REGION_ID); // REGION_ID contains the unsubscribe method
If you want to use the the regiondId you get from the state place your code inside the subscribe callback:
tore.subscribe(() => {
const REGION_ID = store.getState().regionId;
console.log(REGION_ID );
})
This line const REGION_ID = return store.getState().regionId; produces an error because you can't return outside of a function.