I am coming across a reusability conundrum in my React/Redux app. I have coded reusable React components, such as dropdowns and sliders, and I am storing the state values in my Redux Store. It is dawning on me now that anytime any JSX instance of the component changes state, all other instances of that component also change state. They all begin to mimic each other, whereas I need them to operate independently. I also want to initialize state differently in the different instances of the component, and can't figure out a way to do this. For example, for a slider, I want the beginning and ending value of the slider range to be different in different instances of the component. This would be easy to do through React's normal props system, but I am getting confused on how to do it via Redux.
I've researched this a bit and it seems the solution is to add unique IDs to each instance of the component in the Redux store. But I need specifics on where to add the ID (the reducer? the action creator? the component?) and how to have everything flow so that the right component is getting their state changed.
This is my reducer:
const INITIAL_STATE = {
slots: 100,
step: 5,
start: 35,
end: 55,
};
const sliderReducer = (state=INITIAL_STATE, action ) => {
switch (action.type) {
case "MIN_SELECTED":
console.log("Min Selected");
return {
...state,
start: action.payload
};
case "MAX_SELECTED":
console.log("Max Selected");
return {
...state,
end: action.payload
};
default:
return state;
}
}
export default sliderReducer;
this is my action creator:
//Slider Minimum Value Selection Action Creator
export const selectMinValue = (value) => {
return (dispatch) => {
dispatch({
type: "MIN_SELECTED",
payload: value
});
};
};
//Slider Maximum Value Selection Action Creator
export const selectMaxValue = (value) => {
return (dispatch) => {
dispatch({
type: "MAX_SELECTED",
payload: value
});
};
};
the component code is too long to paste, but it's event handlers for onDrag events and JSX for the slider essentially.
You could give each instance a unique id. This would allow you to pass the id to the redux store where you could hold an object or array that would store each instance's data based off on the unique key. You could hold another redux variable that handles unique id generation.
For example you could have a state value for each reusable component called "id" initialized to "null". In the redux store, you could have another "id_counter" that keeps track of the next usable id to assign to a reusable component. During component initialization, you can make a call to redux to retrieve the most recent "id_counter" and then increment "id_counter" by 1. If you start "id_counter" at 0, you can have id's that can access a zero-based array. So, then you can have another item in the redux store, "data", that is an array that grows with each new id that is added. This "data" variable can hold data for each of your reusable components.
Related
I have React app with Redux that has following structure:
<ComponentParent>
<ComponentA></ComponentA>
<ComponentB></ComponentB>
</ComponentParent>
In component A an ComponentDidMount, a fetch is called and data is return async-ly. Reducer is then called to add data to the store.
Component B then accesses the store to access data added by A to the store.
Predictably Component B accesses the data before Component A had a change to write data to the store (because data is coming from aync fetch).
Question:
what is a proper way to design such interaction?
Do I need use
approach similar to
react redux with asynchronous fetch
? Note that in Reducer I just store data returned async-ly by
Component A, unlike in the link
Thanks
Set a default state to your componentB for it to load while awaiting results from your fetch.
In your fetch action, assuming you use redux-thunk:
let fetchData = () => async dispatch => {
let res = await fetchFromDataSource();
dispatch({
type: UPDATE_STATE,
payload: res
})
};
Your component B should be linked up to the store. Upon dispatch update, it should trigger your componentB to reload via ComponentDidUpdate.
I like the pattern of creating an initial state for the object in the reducer, so that any component accessing it gets that initial state first, and can later update based on a post-fetch state.
xReducer.js
const initState = {
// Initial state of object
};
export default function xReducer(state=initState, action) {
switch (action.type) {
case actionTypes.MY_POST_FETCH_ACTION_TYPE:
return {
...state,
// state override
};
default:
return state;
}
}
I am refactoring some code and turning my class components into function components as a way of learning how to use Hooks and Effects. My code uses Redux for state management and axios for database requests with Thunk as middleware for handling asynchronicity. I'm having an issue in one component that does a get request to retrieve a list of customers on what used to be componentDidMount. No matter what I try, the useEffect function gets into an infinite loop and continues requesting the customer list.
The component in question, CustomersTable, gets a list of customers from the database and displays it in a table. The component is wrapped by a container component that uses Redux's connect to pass in the retrieved list of customers to the CustomersTable as a prop.
useEffect(() => {
loadCustomers(currentPage, itemsPerPage, sortProp, (ascending ? 'asc' : 'desc'), {});
}, []);
loadCustomers is a Redux action that uses axios to fetch the customer list. currentPage, itemsPerPage, sortProp and ascending are state variables that are initialized to specific values on 'component mount'
I would expect that because I use the empty array that this would run only once. Instead it runs continuously. I can't figure out why this is happening. My best guess is that when redux gets the list, it returns a new object for state and therefore the props change every time, which then triggers a re-render, which then fetches a new list. Am I using this wrong in that Redux isn't meant to be used with hooks like this?
I ended up getting this working by adding the following:
useEffect(() => {
if (!list.length) {
loadCustomers(currentPage, itemsPerPage, sortProp, (ascending ? 'asc' : 'desc'), {});
}
}, []);
I'm not sure this is the behavior I truly want though. If the list of customers was truly 0, then the code would continue to fetch the list. If the list were truly empty, then I would want it to fetch only once and then stop. Edit: Turns out this definitely doesn't work. It works for the initial load, but breaks the code for any delete or edit.
OK, providing more context here. The container component that wraps the CustomersTable is:
import { connect } from 'react-redux';
import loadCustomers from './actions/customersActions';
import { deleteCustomer } from './actions/customerActions';
import CustomersTable from './CustomersTableHooks';
function mapStateToProps(state) {
return {
customers: state.customers,
customer: state.customer
};
}
export default connect(mapStateToProps, { loadCustomers, deleteCustomer })(CustomersTable);
The action, loadCustomers is:
export default function loadCustomers(page = 1, itemsPerPage = 50, sortProp = 'id', sortOrder = 'asc', search = {}) {
return (dispatch) => {
dispatch(loadCustomersBegin());
return loadCustomersApi(page, itemsPerPage, sortProp, sortOrder, search)
.then(data => dispatch(loadCustomersSuccess(data)))
.catch(() => dispatch(loadCustomersFailure()));
};
}
the reducer for customers is:
export default function customersReducer(state = initialState, action) {
switch (action.type) {
case types.LOAD_CUSTOMERS_BEGIN:
return Object.assign({}, state, { isLoading: true, list: [], totalItems: 0 });
case types.LOAD_CUSTOMERS_SUCCESS:
return Object.assign({}, state, { isLoading: false, list: action.customers || [], totalItems: action.totalItems });
case types.LOAD_CUSTOMERS_FAILURE:
return Object.assign({}, state, { isLoading: false, list: [], totalItems: 0 });
default:
return state;
}
}
I unfortunately can't post much of the CustomersTable itself because things are named in a way that would tell you what company I'm working for.
So, if i understand your code correctly, you are dispatching the loadCustomers action in child component within useEffect but you read actual data in parents mapStateToProps.
That would, of course, create infinite loop as:
parent reads customers from store (or anything from the store, for that matter)
renders children
child fetches customers in useEffect
properties on parent change and cause re-render
whole story goes on forever
Moral of the story: don't dispatch from presentational components. or, in other words, dispatch an action from the same component you read those same properties from store.
On every render you get new customer object because mapStateToProps if doing shallow equal. You could use memoized selectors to get customers, and it won't rerender when is not needed to.
import { createSelectorCreator, defaultMemoize } from 'reselect';
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, deepEqual);
const customerSelector = createDeepEqualSelector(
[state => state.customerReducer.customers],
customers => customers,
);
I disagree with the most voted answer here.
properties on parent change and cause re-render
whole story goes on forever
A re-render will not call the function argument of useEffect when the 2nd argument of dependencies is and empty array. This function will only be called the 1st time, similar to the life cycle method componentDidMount. And it seems like this topic was created because that correctly expected behavior wasn't occurring.
It seems like what is happening is that the component is being unmounted and then mounted again. I had this issue and it brought me here. Unfortunately, without the component code we can only guess the actual cause. I thought it was related to react-redux connect but it turns out it wasn't. In my case the issue was that someone had a component definition within a component and the component was being re-defined / recreated on every render. This seems like a similar issue. I ended up wrapping that nested definition in useCallback.
I'm building Multiple Select component for user to select the seasons on the post. So use can choose Spring and Fall. Here I'm using reselect to track selected objects.
My problem is that my reselect doesn't trigger one it renders at the first time. This question looks pretty long but it has many console.log() lines for clarification. So please bear with me! :)
('main.js') Here is my modal Component. Data for this.state.items is seasons = [{id: '100', value: 'All',}, { id: '101', value: 'Spring',}, { ... }]
return (
<SelectorModal
isSelectorVisible={this.state.isSelectorVisible}
multiple={this.state.multiple}
items={this.state.items}
hideSelector={this._hideSelector}
selectAction={this._selectAction}
seasonSelectAction={this._seasonSelectAction}
/>
('main.js') As you can see I pass _seasonSelectAction to handle selecting items. (It adds/removes an object to/from the array of this.state.selectedSeasonIds). selectedSeasonIds: [] is defined in the state. Let's look at the function.
_seasonSelectAction = (id) => {
let newSelectedSeasonIds = [...this.state.selectedSeasonIds, id];
this.setState({selectedSeasonIds : newSelectedSeasonIds});
console.log(this.state.selectedSeasonIds); <- FOR DEBUGGING
console.log(newSelectedSeasonIds); <- For DEBUGGING
}
I confirmed that it prints ids of selected Item. Probably providing code of SelectorModal.js is irrelevant to this question. So let's move on... :)
('main.js') Here is where I called createSelector
function mapStateToProps(state) {
return {
seasons: SelectedSeasonsSelector(state)
}
}
export default connect(mapStateToProps, null)(...);
(selected_seasons.js) Finally, here is the reselect file
import { createSelector } from 'reselect';
// creates select function to pick off the pieces of states
const seasonsSelector = state => state.seasons
const selectedSeasonsSelector = state => state.selectedSeasonIds
const getSeasons = (seasons, selectedSeasonIds) => {
const selectedSeasons = _.filter(
seasons,
season => _.contains(selectedSeasonIds, season.id)
);
console.log('----------------------');
console.log('getSeasons');
console.log(selectedSeasons); <<- You can see this output below
console.log('seaons');
console.log(seasons);
console.log('----------------------');
return selectedSeasons;
}
export default createSelector(
seasonsSelector,
selectedSeasonsSelector,
getSeasons
);
The output for system console is below
----------------------
getSeasons
Array []
seaons
undefined
----------------------
Thank you for reading this whole question and please let me know if you have any question on this problem.
UPDATE As Vlad recommended, I put SelectedSeasonsSelector inside of _renderSeasons but it prints empty result like above every time I select something. I think it can't get state.seasons, state.
_renderSeasons = () => {
console.log(this.state.seasons) // This prints as expected. But this becomes undefined
//when I pass this.state in to the SelectedSeasonsSelector
selectedSeasons = SelectedSeasonsSelector(this.state)
console.log('how work');
console.log(selectedSeasons);
let seasonList = selectedSeasons.map((season) => {
return ' '+season;
})
return seasonList
}
state.seasons and state.selectedSeasonsIds are getting undefined
Looks like you are assuming that this.setState will change redux store, but it won't.
In a _seasonSelectAction method you are calling this.setState that stores selected ids in container's local state.
However selectors are expect ids will be be stored in global redux store.
So you have to pass selected id's to redux store, instead of storing them in a local state. And this parts are looks missing:
dispatch action
use reducer to store this info into redux store.
add mapDispatchToProps handler to your connect
I'm guessing here, but it looks like confusing terms:component local state is not the same as redux store state. First one is local to your component and can be accessed through this.state attributive. Second is global data related to all of your application, stored in redux store and could be accessed by getState redux method.
I so you probably have to decide, whether to stay with redux stack or create pure react component. If pure react is your choice, than you dint have to use selectors, otherwise you have to dispatch action and more likely remove this.state.
A React component OilBarrel connected my redux store to create a container OilBarrelContainer:
// ---- component
class OilBarrel extends Component {
render() {
let data = this.props.data;
...
}
}
// ---- container
function mapStateToProps(state) {
let data = state.oilbarrel.data;
...
}
const OilBarrelContainer = connect(mapStateToProps)(OilBarrel)
// ---- reducer
const oilbarrel = (state = {}, action) => {
let data = state.data;
}
const storeFactory = (server = false, initialState = {}) => {
return applyMiddleware(...middleware(server))(createStore)(
combineReducers({oilbarrel, otherReducer1, otherReducer2}),
initialState
)
}
I find it strange that mapStateToProps() receives the top level state object (the entire state of the application), requiring me to traverse state.oilbarrel.data, when the reducer (conveniently) only receives the branch of the state that belongs to this component.
This limits the ability to reuse this container without knowing where it fits into the state hierarchy. Am I doing something wrong that my mapStateToProps() is receiving the full state?
That is the mapStateToProps behavior. You have to think redux state as a single source of truth (by the way, that is what it really is) independently of the components you have in project. There is no way out, you have to know the exactly hierarchy of you especific data in the state to pass it to your container component.
No this is intentional, because you may want to use other parts of the state inside your component. One option is to keep the selector (mapStateToProps) in a separate file from your component, which will help you reuse the selector, if you app is very large and complex you can also checkout libraries such as reselect which helps you make your selectors more efficient.
Dan Abramov offers a solution for this in his advanced redux course under Colocating Selectors with Reducers.
The idea is that for every reducer, there is a selector, and the selector is only aware of it's reducer structure. The selectors for higher level reducers, wrap the lower level reducer, with their part of the state, and so on.
The example was taken from the course's github:
In the todos reducer file:
export const getVisibleTodos = (state, filter) => {
switch (filter) {
case 'all':
return state;
case 'completed':
return state.filter(t => t.completed);
case 'active':
return state.filter(t => !t.completed);
default:
throw new Error(`Unknown filter: ${filter}.`);
}
};
In the main reducer file:
export const getVisibleTodos = (state, filter) =>
fromTodos.getVisibleTodos(state.todos, filter);
Now you can get every part of your state without knowing the structure. However, it adds a lot of boilerplate.
I understand basically what keys are for: we need to be able to identify changes in a list of children.
What I'm having trouble with is the information flow, and maintaining synchronicity between the states in my store and the states of subcomponents.
For example, say I have a list of <CustomTextInput> which is exactly what it sounds like. Some container with a text input inside of it. Many of them are stored in some <CustomTextInputList> element.
The number of CustomTextInputs in the list can change, they can be added and subtracted. I have a factory with an incrementing counter that issues new keys every time a CustomTextInput is inserted, no matter where it's placed.
I have a CustomTextInputModel type which populates my store. Whenever I change the value inside one of the inputs, it needs to call a callback which dispatches actions in my store. But then how do I know which one to modify, and how can I be sure that the whole list doesn't rerender from changing a single instance since the entire store's state is being recreated? Do I need to store a reference to some model ID in every CustomTextInput? Should this be the key?
Manythanks, I'm very new at this :)
But then how do I know which one to modify, and how can I be sure that the whole list doesn't re-render from changing a single instance since the entire store's state is being recreated?
If your component is tied to a list of objects in the redux store, then it will re-render the whole list since the parent would be getting new props. However there are a few ways to avoid the re-render.
Note that this is a pretty advanced post, but hopefully it helps. I find this page to be useful in explaining one way to make a react-redux application more performant in rendering large lists by having each item connected to the redux store and listening to itself via an id that is passed in via a prop.
Edit Heres another good article that illustrates the point: https://medium.com/devtravel/optimizing-react-redux-store-for-high-performance-updates-3ae6f7f1e4c1#.3ltrwmn3z
Inside of the Reducer:
function items(state = {}, action) {
switch (action.type) {
case 'MARK':
const item = state[action.id];
return {
...state,
[action.id]: {...item, marked: !item.marked}
};
default: return state;
}
}
function ids(state = [], action) {
return state;
}
In the list container:
<div>
{
ids.map(id => {
return <Item key={id} id={id} />;
})
}
</div>
// Returns an array of just ids, not whole objects
function mapStateToProps(state) {
return {id: state.ids};
}
Inside of the Item component:
// Uses id that is passed from parent, returns the item at the index equal to the id
function mapStateToProps(state, props) {
const { id } = props;
const { items } = state;
return {
item: items[id],
};
}
const markItem = (id) => ({type: 'MARK', id});
export default connect(
mapStateToProps,
{markItem}
)(Item);
Do I need to store a reference to some model ID in every CustomTextInput?
Yes, you will need some type of key/id to pass to the redux actions when you would like to make a modification to that specific item.
Should this be the key?
Most common way to reference something in the in the redux store would be by id.