I have the following reducer:
const selectEntityReducer = function(entityState, action) {
const selectedEntity = entityState.allEntities.filter(e => (e.id == action.id))[0]
const newStateForSelectEntity = updateObject(entityState, {selectedEntity: selectedEntity});
return newStateForSelectEntity;
};
entityState shape looks like {entities:[{id:1}, {id:2}], selectedEntity:{id:2}}.
I have a React component that is connected to the store like this:
function mapStateToProps(state) {
return {
selectEntity: state.entities.selectEntity
};
}
However, it never re-renders.
If I say selectEntities: state.entities in mapStateToProps, then the component re-renders. But I'm only interested in the selected entity for this component.
I'm aware that enforcing immutability down the entire state tree is required for redux to function properly. But I thought that is what I was doing in selectEntityReducer.
Any extra context can be seen in the full redux code in this gist.
Its happens because you filter old piece of state.
You must copy your entities:
const newEntities = entityState.allEntities.slice();
const newSelectedentities = newEntities.filter.........
Copy piece of ctate if it array and then process it.
Related
I am using the aurelia-store state management library for managing state. This question is not specific to Aurelia store, but actually to redux best practices in general since Aurelia store is very much the same thing.
I have an action that fetches unit updates from an API like so:
export const fetchNewUnits = async (state: State): Promise<State> => {
const fetchedUnits = await apiClient.getUnitsMarkers();
// no new updates so don't trigger change in units
// IS THIS ACCEPTABLE?
if (fetchedUnits.length === 0) {
return {
...state,
highwaterMark: new Date()
};
}
const units: UnitMarker[] = state.units.slice();
_.forEach(fetchedUnits, (newUnit) => {
// look for matching unit in store
const idx = _.findIndex(units, {
imei: newUnit.imei
});
// unit was found in store, do update
if (idx !== -1) {
// replace the unit in the store
const replacement = new UnitMarker({...newUnit});
units.splice(idx, 1, replacement);
}
});
// OR SHOULD I ALWAYS DEEP COPY THE ARRAY REFERENCE AND IT'S OBJECTS
return {
...state,
highwaterMark: new Date(),
units: [...units]
};
};
If I do not have any unit changes (i.e. my store is up to date) can I simply return the state with the spread operator as shown in the first return statement? Is this fine since I did not modify the objects?
Or do I always have to do deep replacements such as:
return {
...state,
highwaterMark: new Date(),
units: [...state.units]
};
even if the objects in the array did not change?
The reason why you’re supposed to create a new object is because React components check for prop changes in order to know when to re-render.
If you simply modify an object and pass it in as a prop again, React won’t know that something changed and will fail to rerender.
So in your case, the question is: do you want to rerender, or not? If you don’t, returning the same object is fine and a simple ‘return state’ will let React know that no rerenders are necessary.
See: Why is the requirement to always return new object with new internal references
Issue (tl;dr)
How can we create a custom redux-orm reducer with redux-toolkit's createSlice?
Is there a simpler, recommended, more elegant or just other solution than the attempt provided in this question?
Details
The example of a custom redux-orm reducer looks as follows (simplified):
function ormReducer(dbState, action) {
const session = orm.session(dbState);
const { Book } = session;
switch (action.type) {
case 'CREATE_BOOK':
Book.create(action.payload);
break;
case 'REMOVE_AUTHOR_FROM_BOOK':
Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
break;
case 'ASSIGN_PUBLISHER':
Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
break;
}
return session.state;
}
It's possible to simplify reducers with the createSlice function of redux-toolkit (based on the redux-toolkit usage-guide):
const ormSlice = createSlice({
name: 'orm',
initialState: [],
reducers: {
createBook(state, action) {},
removeAuthorFromBook(state, action) {},
assignPublisher(state, action) {}
}
})
const { actions, reducer } = ormSlice
export const { createBook, removeAuthorsFromBook, assignPublisher } = actions
export default reducer
However, at the beginning of redux-orm reducer we need to create a session
const session = orm.session(dbState);
then we do our redux-orm reducer magic, and at the end we need to return the state
return session.state;
So we miss something like beforeEachReducer and afterEachReducer methods in the createSlice to add this functionality.
Solution (attempt)
We created a withSession higher-order function that creates the session and returns the new state.
const withSession = reducer => (state, action) => {
const session = orm.session(state);
reducer(session, action);
return session.state;
}
We need to wrap every reducer logic in this withSession.
import { createSlice } from '#reduxjs/toolkit';
import orm from './models/orm'; // defined elsewhere
// also define or import withSession here
const ormSlice = createSlice({
name: 'orm',
initialState: orm.session().state, // we need to provide the initial state
reducers: {
createBook: withSession((session, action) => {
session.Book.create(action.payload);
}),
removeAuthorFromBook: withSession((session, action) => {
session.Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
}),
assignPublisher: withSession((session, action) => {
session.Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
}),
}
})
const { actions, reducer } = ormSlice
export const { createBook, removeAuthorsFromBook, assignPublisher } = actions
export default reducer
This is a fascinating question for me, because I created Redux Toolkit, and I wrote extensively about using Redux-ORM in my "Practical Redux" tutorial series.
Off the top of my head, I'd have to say your withSession() wrapper looks like the best approach for now.
At the same time, I'm not sure that using Redux-ORM and createSlice() together really gets you a lot of benefit. You're not making use of Immer's immutable update capabilities inside, since Redux-ORM is handling updates within the models. The only real benefit in this case is generating the action creators and action types.
You might be better off just calling createAction() separately, and using the original reducer form with the generated action types in the switch statement:
export const createBook = createAction("books/create");
export const removeAuthorFromBook = createAction("books/removeAuthor");
export const assignPublisher = createAction("books/assignPublisher");
export default function ormReducer(dbState, action) {
const session = orm.session(dbState);
const { Book } = session;
switch (action.type) {
case createBook.type:
Book.create(action.payload);
break;
case removeAuthorFromBook.type:
Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
break;
case assignPublisher.type:
Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
break;
}
return session.state;
}
I see what you're saying about adding some kind of "before/after" handlers, but that would add too much complexity. RTK is intended to handle the 80% use case, and the TS types for createSlice are already incredibly complicated. Adding any more complexity here would be bad.
I came across this question looking to combine the benefits of redux-toolkit
and redux-orm. I was able to come up with a solution I've been pretty happy
with so far. Here is what my redux-orm model looks like:
class Book extends Model {
static modelName = 'Book';
// Declare your related fields.
static fields = {
id: attr(), // non-relational field for any value; optional but highly recommended
name: attr(),
// foreign key field
publisherId: fk({
to: 'Publisher',
as: 'publisher',
relatedName: 'books',
}),
authors: many('Author', 'books'),
};
static slice = createSlice({
name: 'BookSlice',
// The "state" (Book) is coming from the redux-orm reducer, and so will
// never be undefined; therefore, `initialState` is not needed.
initialState: undefined,
reducers: {
createBook(Book, action) {
Book.create(action.payload);
},
removeAuthorFromBook(Book, action) {
Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
},
assignPublisher(Book, action) {
Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
}
}
});
toString() {
return `Book: ${this.name}`;
}
// Declare any static or instance methods you need.
}
export default Book;
export const { createBook, removeAuthorFromBook, assignPublisher } = Book.slice.actions;
The redux-toolkit slice is created as a static property on the class, and then
the model and its actions are exported in a manner similar to Ducks
(ORMDucks??).
The only other modification to make is to define a custom updater for
redux-orm's reducer:
const ormReducer = createReducer(orm, function (session, action) {
session.sessionBoundModels.forEach(modelClass => {
if (typeof modelClass.slice.reducer === 'function') {
modelClass.slice.reducer(modelClass, action, session);
}
});
});
See a more complete example here:
https://gist.github.com/JoshuaCWebDeveloper/25a302ec891acb6c4992fe137736160f
Some Notes
#markerikson makes a good point about some of the features of redux-toolkit
not being used since redux-orm is managing the state. For me, the two
greatest benefits of using this method are not having to wrangle a whole
bunch of action creators and not having to contend with awful switch
statements :D.
I am using the stage 3 class fields and static class features proposals. (See
https://babeljs.io/docs/en/babel-plugin-proposal-class-properties). To make
this ES6 compatible, you can easily refactor the model class to define its
static props using the current syntax (i.e. Book.modelName = 'Book';).
If you decide to mix models like the one above with models that don't define
a slice, then you'll need to tweak the logic in the createReducer updater
slightly.
For a real world example, see how I use the model in my project here:
https://github.com/vallerance/react-orcus/blob/70a389000b6cb4a00793b723a25cac52f6da519b/src/redux/models/OrcusApp.js.
This project is still in the early stages. The largest question in my mind is
how well this method will scale; however, I am optimistic that it will continue
to provide numerous benefits as my project matures.
Try using normalized-reducer. It's a higher-order-reducer that takes a schema describing the relationships, and returns a reducer, action, and selectors that write/read according to the relationships.
It also integrates easily with Normalizr and Redux Toolkit.
Good day,
The beginnings of learning redux can be confusing
The steps in this code:
Create reducer for increment
Create Store
If user takes onClick action, function increment dispatch previously mentioned reducer
In this same component display data
Render component inside Provider with store
In this case need is saving the state with data to LocalStorage.
What should I do for save counter_state to local-storage?
// reducer
function counter(state=0, action) {
console.log('counter', action)
switch(action.type) {
case 'INCREMENT':
return state + 1;
return state;
}
}
///create store
const store = createStore(counter);
// React Component
class Counter extends React.Component {
increment() {
this.props.dispatch({
type: 'INCREMENT'
});
}
render() {
return (
<div>
{this.props.state}
<div>
<button onClick={this.increment.bind(this)}>+</button>
</div>
</div>
)
}
}
const mapStateToProps = function (state) {
return {state};
}
const CounterApp = connect(mapStateToProps)(Counter);
class Test5 extends React.Component {
render() {
return (
<Provider store={store}>
<CounterApp />
</Provider>
)
}
}
export default Test5;
localStorage has a pretty simple interface. I'd recommend looking up its API on mdn. Since you're not using any of the persistence libraries you're importing, might as well try it directly for learning purposes:
Write the value of state.counter to localStorage in your mapStateToProps
When you do createStore you can pass a second argument with preloadedState. So before you do that, read from localStorage to get the saved value of your counter.
Some remarks:
state in the reducer is usually an object with properties. It might work with a simple number, but if you want to learn redux you'll want to use an object here.
I'd recommend to move on to the more complicated libraries like redux-persist once you feel comfortable with the basics of redux. It's too much abstraction at once for getting a simple counter to work.
Basic Problem
I've got a Redux store with the following data:
foo: {
currentId: 1,
things: [{id: 1}, {id: 2}),
}
I'd like to make a utility method somewhere (eg. on a Foo singleton object) such that any module in my code can do:
import Foo from 'foo';
foo.getCurrentFoo(); // returns foo.thins[foo.currentId];
but I'm having trouble figuring out where to put it.
Failed Attempt
My initial attempt was to create a Foo component singleton:
// Foo.js
class FooBase extends React.Component {
getCurrentFoo() {
return this.state.foo.things[this.state.foo.currentId];
}
}
const Foor = connect((state) => state.foo)(FooBase);
export default new FooWrapper();
But that doesn't work. Redux complaieds about the property store not existing (when I did new FooWrapper()). That makes sense, because my component isn't inside a <Provider />. However, I just want a stand-alone utility class/object, not something actually in the DOM, which rules out <Provider/>.
How can I make a method like the one described above, that actually works, without involving <Provider /> ... and where do I put it?
The nice thing about the react-redux helpers is that they allow you to use connect() and <Provider /> to automatically pass the store to child components via React's context. However, that doesn't necessarily mean that you have to use these helpers, especially in areas of a codebase that don't use React.
So here lies the problem: connect() and <Provider /> help us by giving our React components access to a singleton instance of a store, but how can we access this store somewhere where connect() and <Provider /> can't be used?
I think the easiest solution here is to create a singleton class that holds on to the store, so any non-React module can still use the store.
So let's say you're creating your store like this:
init.js
import {createStore} from 'redux';
const initialState = {
currentId: 1,
things: ['foo', 'bar']
};
const reducer = (state = initialState, action) => {
if (action.type === 'SET_CURRENT_ID') {
return Object.assign({}, state, {
currentId: action.id
});
}
return state;
};
const store = createStore(reducer);
This store takes an action of type SET_CURRENT_ID which simply returns a new state with the currentId property changed to whatever was handed to it. You could then get the current "thing" by doing something like store.getState().things[store.getState().currentId]. So let's create a Singleton class that can hold on to the store and provide a wrapper around this functionality.
store.js
class Store {
constructor() {
this._store = undefined;
}
setStore(store) {
this._store = store;
}
getCurrentThing() {
if (this._store) {
const {things, currentId} = this._store.getState();
return things[currentId];
}
}
setCurrentThing(id) {
if (this._store) {
const action = {
type: 'SET_CURRENT_ID',
id
};
this._store.dispatch(action);
}
}
}
export let singletonStore = new Store();
This class creates an instance the first time it is used, and uses that instance every subsequent time. So when you originally create your store, simply import this class and call setStore().
init.js
import {singletonStore} from './store';
singletonStore.setStore(store);
Then, every subsequent file where singletonStore is used will have the same state.
test.js
import {singletonStore} from './store';
console.log(singletonStore.getCurrentThing()); // 'bar'
singletonStore.setCurrentThing(0);
console.log(singletonStore.getCurrentThing()); // 'foo'
This should work just fine for your need to use your store in modules that don't have the benefit of being passed the store magically with connect() and <Provider />.
I'm trying to build keyboard shortcut support into my React/Redux app in an idiomatic React/Redux way. The way I am planning to do this is to have the following action creator and associated action:
registerShortcut(keyCode, actionCreatorFuncReference)
The reducer would then update a registeredShortcuts object in the redux store with a mapping of keyCodes to actionCreatorFuncReferences. Then my root component would listen for keyup and see if there is an associated keyCode registered and if so, then dispatch the mapped action via the action creator function reference.
However, this would be the first time I am storing function references in my Redux store. To date, I've only had objects with keys with vanilla values (strings, ints, etc).
The Redux docs says:
You should do your best to keep the state serializable. Don’t put anything inside it that you can’t easily turn into JSON.
Does this suggest it's a bad idea to store such function references in my Redux store? If so, what is a better way to accomplish what I'm trying to do in React/Redux?
An alternative approach is just to store the mapping of keyCodes and function references in the root react component itself, but that didn't feel very Redux-like since now the application state is not in the Redux store.
No, you should not store function references in the redux store. They are not serializable, and as you mentioned state should be serializable at all times. The most redux-friendly approach I can think of is just to keep the map of hotkeys to their actionCreatorFuncNames.
TL;DR: You don't. The store state must be serializable at all times (as Nathan answered).
The Redux way is via enhancers, or the Redux-Observable way via dependencies.
NL;PR: Based on the Redux docs example, what you want is to pass the reference in your action(1), ignore it your reducer(2) and use it in your enhancer(3):
//... in your action:
const data={val:1}, ref=()=>{};
const action = {type:'ACTION_WITH_REF', data, ref}; //(1)
//... in your reducer:
case 'ACTION_WITH_REF':
return {...state, data: action.data}; //(2)
//... and in your enhancer:
import { createStore, applyMiddleware } from 'redux';
import reducers from './reducers';
export const myRefStore= {};
function refHandler({ getState }) {
return next => action => {
switch(action.type){
// this can be done more elegantly with a redux-observable
case 'ACTION_WITH_REF':
myRefStore.aRef = action.ref; // (3)
break;
}
// be sure to maintain the chain of the store
const returnValue = next(action);
// otherwise, your midddeware will break the store
return returnValue;
};
}
const store = createStore(
reducers,
initialState,
applyMiddleware(refHandler)
);
Note: As far as there are no side-effects in your enhancers, you are good to go. Be aware that you could have obtained the refs directly in the reducers, but such an approach keeps the reference at the reducer-level and misses the point of combineReducers(). With an enhancer, you keep them all in one place(myRefStore).
One final observation is that a redux store is not an any-data store but a state store, thus why we need to handle functions and other non-state related stuff in enhancers. You can leverage the enhancer backbone to Redux-Observable and inject myRefStore via dependencies.
I'm new to redux, but the way I see it, you could pass the key code and an action type.
Then a reducer could be listening for that action type and make changes accordingly.
Here is an example using the library Mousetrap:
// On your Container
function registerShortcut(element, dispatch, keyCode, actionType) {
Mousetrap(element).bind(keyCode, function(e) {
dispatch({
type: actionType,
payload: {
keyCode: keyCode,
event: e
}
});
});
});
mapDispatchToProps = function(dispatch) {
return {
onMount: function(element) {
registerShortcut(element, dispatch, ['command+f', 'ctrl+f'], 'OPEN_SEARCH');
},
onUnmount: function(element) {
Mousetrap(element).unbind(['command+f', 'ctrl+f']);
}
};
};
// On your Component
componentDidMount() {
onMount(ReactDOM.findDOMNode(this));
};
componentWillUnmount() {
onUnmount(ReactDOM.findDOMNode(this));
};
// On your reducer
function reducer(oldState, action) {
if (action.type == 'OPEN_SEARCH') {
//... make changes ...//
return newState;
}
return oldState;
};
This way, keyboard shortcuts will dispatch an action. The reducer will make the changes necessary to the state. And finally, the application can re-render.