Is there any particular "good practice" in Redux when it comes to reducing state of dynamically created items? In this particular case, I'm dealing with a list of users that may join/leave the app, tables and games at any time.
let userReducer = (user, action) => {
switch(action.type) {
case 'table:create':
case 'table:join': return {
...user,
tables: [...user.tables, action.tableId]
}
case 'table:leave': return {
...user,
tables: user.tables.filter(tableId => tableId != action.tableId)
};
case 'game:join': return {
...user,
games: [...user.games, action.gameId]
};
case 'game:leave': return {
...user,
games: user.games.filter(gameId => gameId != action.gameId)
};
}
}
let usersById = (users = {}, action) => {
let user = users[action.userId];
switch(action.type) {
case 'user:join': return {
...users,
[action.user.id]: action.user
};
case 'user:leave': {
users = {...users};
delete users[action.userId];
return users;
};
case 'table:create':
case 'table:join':
case 'table:leave':
case 'game:join':
case 'game:leave': return {
...users,
[action.userId]: userReducer(user, action)
};
}
return users;
}
The last five cases in the second function's switch statement look particularly ugly to me. Maybe I could just condense it with an if? (if user is defined, then apply userReducer to it).
let usersById = (users = {}, action) => {
let user = users[action.userId];
if(user)
return {
...users,
[user.id]: userReducer(user, action);
}
switch(action.type) {
case 'user:join': return {
...users,
[action.user.id]: action.user
};
case 'user:leave': {
users = {...users};
delete users[action.userId];
return users;
};
}
return users;
}
I don't think there is any good practice in order to create reducers.
Personally I rather use the approach of your first exemple, as it make your code more readable. In addition it will allow you to keep the same structure to all your reducers.
On the contrary, that looks like some fairly well-organized reducer logic. But yes, if you want to use an if statement like that, you absolutely can - per the Redux FAQ on using switch statements, it's fine to use whatever logic approach you want in a reducer.
For more information on ways to organize reducer logic, see the Redux docs section on "Structuring Reducers", and my recent blog post Idiomatic Redux: The Tao of Redux, Part 2 - Practice and Philosophy
Related
I'm looking for a smarter way to enforce
one value true, setting the others to false
This Python q&a suggests an enum
(I'm not familiar with either)
I can already
switch (action.type) {
case 'loadCurrent':
return {...state,
loadCurrent: true,
loadPrev: false, etc ...};
But this is a lot of boilerplate and intuition (as well as that linked python question) tell me there's a more elegant solution.
function reducer (state, action) {
switch (action.type) {
case 'loadCurrent':
return {...state, loadCurrent: true};
case 'fetchNew':
return {...state, fetchNew: true};
case 'loadPrevHook':
return {...state, loadPrevHook: true };
case 'loadNextHook':
return {...state, loadNextHook: true };
case 'loaded':
return {...state, loaded: true };
}
}
const initialState = {
loadCurrent: false,
fetchNew: false,
loadPrevHook: false,
loadNextHook: false,
loaded: true }
Having separate properties for these doesn't make the most sense. Put all the data into a single property instead - perhaps call it loadStage or whatever you think is appropriate.
If, as the code here suggests, the state doesn't have any other properties, just setting the action.type looks like it'd do.
function reducer (state, action) {
return { loadStage: action.type };
}
If you expanded the reducer to include other actions as well not related to the loading actions, you could make an array corresponding to the loading actions.
const loadStageActions = ['loadCurrent', 'fetchNew', 'loadPrevHook', 'loadNextHook', 'loaded'];
function reducer (state, action) {
if (loadStageActions.includes(action.type)) {
return { ...state, loadStage: action.type };
}
// implement other logic here
}
Then, in the rest of your code, just check the one string property instead of the multiple booleans. Eg instead of doing state.loadCurrent, do state.loadStage === 'loadCurrent'.
I'm quite new to coding and I'm currently practicing the useReducer() hook in React to manage some state in a simple todo app.
I'm having trouble when trying to implement the TOGGLE_TODO action. I've done it before using arrays, but as I'll likely be working with a lot of objects, I'm trying to figure out why I can't get this right. I'd say I'm learning by failing, but all I'm learning is how to switch the computer off and walk away!
Each time I toggle, I'm passing the state with the spread operator, I've tried it throughout all of the item, I've logged out the key and action.payload to make sure I'm getting a match (it works when I do a simple alert with matching).
I'm aware that the toggle isn't a toggle yet, I was just trying to simply get complete to be true.
I've tried a multitude of things to return state, I've added return to the beginning of the statement, and I"ve encountered some weird bugs along the way. As mentioned, this is quite simple state for now, but it will be more complex in another project I'm working on, so useState get's quite messy.
Any help on what I'm doing wrong here would be highly appreciated.
const initialAppState = {
isOpen: true,
todos: {}
};
export const ACTIONS = {
TOGGLE_MODAL: "toggle-modal",
ADD_TODO: "add-todo",
TOGGLE_TODO: "toggle-todo"
};
const reducer = (state, action) => {
// switch statement for actions
switch (action.type) {
case ACTIONS.TOGGLE_MODAL:
return { ...state, isOpen: !state.isOpen };
case ACTIONS.ADD_TODO:
return {
...state,
todos: {
...state.todos,
// Object is created with Unix code as the key
[Date.now()]: {
todo: action.payload.todo,
complete: false
}
}
};
case ACTIONS.TOGGLE_TODO:
// Comparing the key and the action payload. If they match, it should set complete to 'true'. This will be updated to a toggle when working.
Object.keys(state.todos).map((key) => {
if (key === action.payload) {
return {
...state,
todos: { ...state.todos, [key]: { complete: true } }
};
}
return state;
});
default:
throw new Error("Nope. not working");
}
};
In the render, I pass the key as an id so it can get returned with the payload.
Here is the dispatch function from the component...
const Todo = ({ id, value, dispatch }) => {
return (
<div className="todo">
<h1>{`Todo: ${value.todo}`}</h1>
<p>Done? {`${value.complete}`}</p>
<button
onClick={() =>
dispatch({
type: ACTIONS.TOGGLE_TODO,
payload: id
})
}
>
Mark as Done
</button>
</div>
);
};
and the render is using Object.entries which all works just fine. There were times when I'd get an error, or the initial todo would disappear, so I knew that state wasn't being updated correctly.
Here is the code on CodeSandbox too. I'll update here if I get it working, but I've been stuck here a couple of days. :-(
You were almost there, good idea to index your items with Date.now()!
Only a few issues in the TOGGLE_TODO case:
your reducer should always return a state, your return statement should be at the end of the case, but you put it with the map's function
your reducer should compute a new state, not mutate the current state. So you have to create a new todo object with the complete property.
Here is how it goes:
case ACTIONS.TOGGLE_TODO:
const newTodos = Object.keys(state.todos).map((key) => {
if (key === action.payload) {
return { ...state.todos[key], complete: true } // create a new todo item
}
else {
return state.todos[key]; // keep the existing item
}
});
return {...state, todos: newTodos};
I have a redux store. To change the data in the store, the typical way is to create an action, actionCreator, a reducer and then dispatch the action.
For a small to medium sized app, it looks like an overkill to change at so many place to reflect such changes. So I created a generic reducer which looks something like this :
// here state is a copy of actual state, so I can mutate it directly
const reducer = ( state, action) => {
if(action.type == 'SETTER'){
try{
return assign(state, action.data.target, action.data.value )
}
catch(err){
console.log('WARNING: the key wasn\'t valid', err)
}
}
return state;
}
this assign method goes like this:
const assign = (obj, prop, value) => {
if (typeof prop === "string")
prop = prop.split(".");
if (prop.length > 1) {
var e = prop.shift();
assign(obj[e] , prop, value);
} else
obj[prop[0]] = value;
return obj
}
Then I have a a generic action dispatcher and a container component, which allow me to do something like this :
containerComponent.set( 'user.name', 'Roy' )
containerComponent.set( 'order.receiver.address', 'N/A')
The action which fires when set is called on the containerComponent looks like this :
{
type : 'SETTER',
data : {
target : 'user.name',
value : 'Roy'
}
}
As you can see, this generic reducer allows me to never write a reducer again, but I am still dispatching an action whenever state changes, so no violation of any of the core principles of redux.
Are there any minor/major shortcomings in this approach, especially in terms of performance? And also, where do you find this approach to be useful.
As you noted quite right, Redux requires you to implement multiple layers of indirection between the point where something in your application happens, and the point where the store is actually updated to reflect this event.
This is by design.
Global state generally creates the problem that it can be changed arbitrarily from anywhere in your application, without a simple way of understanding how or why. And yes, a Redux store is effectively global state.
By separating the questions of what happened (as represented by an action and described by the action's payload) from how does that affect the global state (as defined by the reducer), Redux removes this issue to a certain degree. Instead of allowing arbitrary changes to the global state, only certain, very specific combinations of changes can be made, triggered by well-defined events that in the best case come with enough semantic information attached to them to enable tracing them back to their origin.
By undermining this core idea of Redux by creating a single pair of generic action and reducer, you loose one of the core advantages of Redux, and are left with a set of indirections and abstractions between your components and the store that don't really bring you any significant benefits.
It is common wisdom that code that doesn't create value is code best deleted. In my mind, you may be much better off not using Redux and simply using component state instead rather than using a crippled implementation of Redux.
An interesting read regarding this topic from Dan Abramov, who created Redux: You might not need Redux.
Timo's answer does a good job explaining why your implementation sort of goes against a lot of the principles of Redux. I would just add that you might find Mobx interesting. I think it more closely resembles the sort of state management you're trying to get.
This function (assign called by reducer) is not according Redux rules, this is not immutable 'pure' function because mutate state.
Test:
const assign = (obj, prop, value) => {
if (typeof prop === "string")
prop = prop.split(".");
if (prop.length > 1) {
var e = prop.shift();
assign(obj[e], prop, value);
} else
obj[prop[0]] = value;
return obj
}
const reducer = (state, action) => {
if (action.type == 'SETTER') {
try {
return assign(state, action.data.target, action.data.value)
}
catch (err) {
console.log('WARNING: the key wasn\'t valid', err)
}
}
return state;
}
const testReducer = () => {
const user = {
id: 0,
name: ''
};
const action = {
type: 'SETTER',
data: {
target: 'name',
value: 'Roy'
}
};
console.log('user before: ', user);
reducer(user, action);
console.log('user after: ', user);
};
testReducer();
Test results:
user before: { id: 0, name: '' }
user after: { id: 0, name: 'Roy' }
Easiest fix:
const assign = (obj, prop, value) => {
var tempObj = Object.assign({}, obj);
if (typeof prop === "string")
prop = prop.split(".");
if (prop.length > 1) {
var e = prop.shift();
assign(tempObj[e], prop, value);
} else
tempObj[prop[0]] = value;
return tempObj
}
EDIT
Fix without copy the values of the state object to temp target object:
const assign = (obj, prop, value) => {
if (typeof prop === "string")
prop = prop.split(".");
if (prop.length > 1) {
var e = prop.shift();
assign(obj[e], prop, value);
} else {
return {
...obj,
[prop[0]]: value
};
}
}
Reducer are now simple functions and can be easily reuse
const getData = (state, action) => {
return {...state, data: state.data.concat(action.payload)};
};
const removeLast = (state) => {
return {...state, data: state.data.filter(x=>x !== state.data[state.data.length-1])};
}
Action type and reducer function are now declared in an array
const actions = [
{type: 'GET_DATA', reducer: getData},
{type: 'REMOVE_LAST', reducer: removeLast},
{type: 'REMOVE_FIRST', reducer: removeFirst},
{type: 'REMOVE_ALL', reducer: removeAll},
{type: 'REMOVE_BY_INDEX', reducer: removeByIndex}
];
Initial state for the reducer
const initialState = {
data: []
}
actionGenerators creates an unique Id using Symbol and assign that Id to actions and reducer function.
const actionGenerators = (actions) => {
return actions.reduce((a,c)=>{
const id = Symbol(c.type);
a.actions = {...a.actions, [c.type]: id};
a.reducer = a.reducer ? a.reducer.concat({id, reducer: c.reducer}) : [{id, reducer: c.reducer}];
return a;
},{});
}
reducerGenerators is a generic reducer creator.
const reducerGenerators = (initialState, reducer) => {
return (state = initialState, action) => {
const found = reducer.find(x=>x.id === action.type);
return found ? found.reducer(state, action) : state;
}
}
Usage
const actionsReducerCreator = actionGenerators(actions);
const store = createStore(reducerGenerators(initialState, actionsReducerCreator.reducer));
const {GET_DATA} = actionsReducerCreator.actions;
store.dispatch({type: GET_DATA});
I have implemented this in my todo application on my github
Redux-Reducer-Generator
I'm learning to build a pokedex app that gives you info about a pokemon after you type in a name and hit submit. It has to go through 2 calls to get the right info, with the 1st getting its height and weight, and then 2nd getting its description (ex. "Charmander can usually be found in hot areas..."). Below is my action breakdown.
export const fetchPokemon = function (pokemonName) {
return function (dispatch) {
dispatch(requestPokemon(pokemonName))
const requestURL = `http://pokeapi.co/api/v2/pokemon/${pokemonName}/`
return $.ajax({
url: requestURL,
}).done(function (data) {
dispatch(receivePokemon(data))
return fetchPokemonDescription(pokemonName)
}).done(function (res) {
dispatch(receivePokemonDescription(res))
})
}
}
...
export const fetchPokemonDescription = function (pokemonName) {
return function (dispatch) {
dispatch(requestPokemonDescription(pokemonName))
const requestURL = `http://pokeapi.co/api/v2/pokemon-species/${pokemonName}/`
return $.ajax({
url: requestURL,
})
}
}
Should I have a separate reducer for each call? Looking at the docs about reducer composition I'm not sure if it would make it cleaner to have 1 reducer vs 2 reducers. The 2 calls are not dependent on each other per say, but each different pokemon input will need to make both calls and the data coming back from both belongs to one pokemon so I was thinking it should be in one reducer handling one part of the state.
I would use 1 reducer in your case.
It would be useful to see your state structure, but I think that you have something like:
currentPokemon: {
name: ...
height: ...
weight: ...
description: ...
}
If this is the case, I would use 1 reducer, because you manage just 1 branch of the state (currentPokemon).
The reducer will switch the action: in one case it will update height and weight, in the other the description:
export default function currentPokemonReducer (state = initialState, action) {
switch(action.type) {
case DATA_RECEIVED:
return {
...state,
height: action.payload.height,
weight: action.payload.weight
}
case DESCRIPTION_RECEIVED:
return {
...state,
description: action.payload
}
...
default:
return state;
}
}
The AJAX call you are making should be paired with an action type. You should then return the the type and your payload, which I'm assuming based on your code, you intend to be to request responses.
//Now sure where these functional are define or what they do, but I'll assume they are selectors of some sort?
export const fetchPokemon = function (pokemonName) {
return function (dispatch) {
dispatch(requestPokemon(pokemonName))
const requestURL = `http://pokeapi.co/api/v2/pokemon/${pokemonName}/`
return $.ajax({
url: requestURL,
}).done(function (data) {
dispatch(receivePokemon(data))
return fetchPokemonDescription(pokemonName)
}).done(function (res) {
dispatch(receivePokemonDescription(res))
})
}
}
you're action might look something like this:
const data = fetchPokemon(Charmander);
//this can be defined somewhere else in your app, should not be in your actions file.
export const pokemonAction(data) {
return {
type: "GET_POKEMON_NAME",
payload: data.name
}
}
//repeat for any other call
Best practice is to have your types defined separately so that they can be pulled into you actions files and reducer files. A lot of how you build your actions and reducers will depend on your response objects.
export const getPokemonReducer(state= {}, action) {
if(action.type === "GET_POKEMON_NAME") {
return {
...state,
[pokemonName]: action.payload
}
if(action.type === "GET_POKEMON_DESC") {
return {
...state,
[pokemonDesc]: action.payload
}
}
Reducers can be used in many different ways depending on how you want to shape your state. You should think about how you would like to use this information through out your application. The above example would give a single property, lets call it "Pokemon", and that properties value is an object that the above two properties.
For example if you want to pull in just the name and don't want to pull it in as this.props.pokemon.pokemonName then maybe you would consider having a separate reducer. I would also consider looking into abstractions such as axios and redux-promise which make asynchronous calls much easier to manage in redux.
This is a hacky-ish question about mixing redux with 3rd party libraries. I'm well aware that this is contrary to the Redux guides. It's mostly for discussion and exploration - I also didn't want to pollute their Github issues ;)
The third party data
We're using a relational and immutable data structure module to store our application data. In a nutshell, the module works a little like a relational database:
It exposes Tables
Tables contain the application data
Tables have 4 methods: get, post, put, delete
Data objects dynamically reference each other by index/primary key
Current Redux Store structure
Since we're using Redux, we initially chose NOT to expose the tables directly, since they contain methods. We instead expose the result of Table.get();, which returns an array of it's data objects.
So our reducers "update state" by working with the 3rd party module. The heavy-lifting is done by the 3rd party, the reducers basically always return Table.get();.
Our store looks something like this:
// the application data (built by relational-json)
var appDB = {
People: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
},
Organization: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
},
Address: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
}
};
// example Reducer
store = createStore(combineReducers({
Person: function personReducer(state, action) {
"use strict";
switch (action.type) {
case "UPDATE_PERSON_LIST":
case "UPDATE_PERSON": {
appDB.Person.put(action.data, "Person");
return appDB.Person.get();
}
case "CREATE_PERSON": {
appDB.Person.post(action.data, "Person");
return appDB.Person.get();
}
default: {
return appDB.Person.get();
}
}
},
Organization: function personReducer(state, action) {
"use strict";
switch (action.type) {
case "UPDATE_ADDRESS_LIST":
case "UPDATE_ADDRESS": {
appDB.Organization.put(action.data, "Organization");
return appDB.Organization.get();
}
case "CREATE_ADDRESS": {
appDB.Organization.post(action.data, "Organization");
return appDB.Organization.get();
}
default: {
return appDB.Organization.get();
}
}
},
Address: function personReducer(state, action) {
"use strict";
switch (action.type) {
case "UPDATE_ADDRESS_LIST":
case "UPDATE_ADDRESS": {
appDB.Address.put(action.data, "Address");
return appDB.Address.get();
}
case "CREATE_ADDRESS": {
appDB.Address.post(action.data, "Address");
return appDB.Address.get();
}
default: {
return appDB.Address.get();
}
}
}
}));
// resulting initial state looks like:
var state = {
Person: [],
Organization: [],
Address: []
};
Our actual setup looks like above, but with close to 100 reducers. Most of the reducers are extremely identical, too. Their only changes are often the type of the action and the Table to update.
THE QUESTION
Alternative Store structure?
We're contemplating having a single reducer to handle the 3rd party data, and exposing the "Tables".get() in the Store structure. Our store would then have a much simpler structure (and much fewer reducers), and look something like:
// the application data (built by relational-json)
var appDB = {
People: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
},
Organization: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
},
Address: {
get: function() {},
post: function() {},
put: function() {},
delete: function() {}
}
};
// example Reducer
store = createStore(combineReducers({
appDB: function dbReducer(state, action) {
"use strict";
switch (action.type) {
case "UPDATE_PERSON_LIST":
case "UPDATE_PERSON": {
appDB.Person.put(action.data, "Person");
break;
}
case "CREATE_PERSON": {
appDB.Person.post(action.data, "Person");
break;
}
case "UPDATE_ORGANIZATION_LIST":
case "UPDATE_ORGANIZATION": {
appDB.Organization.put(action.data, "Organization");
break;
}
case "CREATE_ORGANIZATION": {
appDB.Organization.post(action.data, "Organization");
break;
}
case "UPDATE_ADDRESS_LIST":
case "UPDATE_ADDRESS": {
appDB.Address.put(action.data, "Address");
break;
}
case "CREATE_ADDRESS": {
appDB.Address.post(action.data, "Address");
break;
}
default: {
break;
}
}
return Object.keys(appDB).reduce(function(obj, key) {
obj[key] = appDB[key].get;
return obj;
}, {})
}
}));
// resulting initial state looks like:
var state = {
appDB: {
People: function get() {},
Organization: function get() {},
Address: function get() {}
}
};
Where things conflict is that apis.apiX would be an object of methods mentioned above (get). The data isn't directly exposed, it has to be exposed by using Table.get();
This doesn't cause a problem in our case since we get data by using selectors (reselect), and they know when the data has changed, even if they have to go through Table.get();
Where I'm uneasy/unsure is for anything else expected or done by Redux. Would such a structure break things?
Both of these options are opposite to how Redux apps work because you rely on data mutations rather than return new objects.
While technically you can mutate data with Redux I think it's more trouble than it's worth in this scenario because you don't really get benefits of Redux. For example, the views can't redraw efficiently because mutations make it impossible to tell from a glance which data has changed.
This is why I suggest you to not use Redux if you want to treat a specific client-side DB as the source of truth. Redux only works well if it is the source of truth. So just use the DB directly and/or build an abstraction around it that better fits what you are trying to do.
From what I understand, Redux doesn't care what your store looks like. It's an abstraction. You decide how it looks. You also decide how actions affect it with reducers. Deciding to expose methods and/or data is irrelevant honestly. Nothing will break as long as you stick with your design decisions. The only thing I would be wary of is whether or not your application state is truly immutable by exposing methods rather than only data.
Besides that, knowing that your application state is reflected by third-party methods implies you develop components with that in mind. It may not be the agreed upon ideal example, but that doesn't mean you can't do it. That's my opinion anyway.
You may want to look into redux-orm. It provides a similar "model"-like API around Redux-style immutable reducers. Still new, but seems pretty useful so far.