In lesson 16 of the egghead.io series on Redux, I tried to implement my own combineReducers function before looking at how Dan did it. I got as far as the following. I tried to use for ... in on the sub-reducers (todos, visibilityFilter) passed in like so
const combineReducers = (reducers) => {
return (state,action) => {
let temp = {};
for (let i in reducers) {
temp[i] = reducers[i](state,action)
}
return temp;
}
}
This does not work. When I test it with the expect library, I receive the following error in the console. Strangely, if I'm not mistaken, it looks like the state from the call to the todos reducer has been nested in to the call of the visibilityFilter reducer. This is very odd as my code shows them being distinctly separate fields in the object returned.
Uncaught Error: Expected { todos: [ { completed: false, id: 1, text:
'Go shopping' } ], visibilityFilter: { todos: [ { completed: false,
id: 0, text: 'Learn Redux' } ], visibilityFilter: 'SHOW_ALL' } } to
equal { todos: [ { completed: false, id: 0, text: 'Learn Redux' }, {
completed: false, id: 1, text: 'Go shopping' } ], visibilityFilter:
'SHOW_ALL' }
My test code is
const testTodoApp = () => {
const stateBefore = {
todos: [{id: 0, text:'Learn Redux', completed: false}],
visibilityFilter: 'SHOW_ALL',
};
// action is an object. with a defined type property.
const action = {
type: 'ADD_TODO',
id: 1,
text: 'Go shopping',
};
const stateAfter = {
todos: [{id: 0, text:'Learn Redux', completed: false},
{id: 1, text:'Go shopping', completed: false},
],
visibilityFilter: 'SHOW_ALL',
};
deepFreeze(stateBefore);
deepFreeze(action);
expect(
todoApp(stateBefore, action)
).toEqual(stateAfter);
console.log("Test passed: todoApp")
}
testTodoApp();
This test will pass if I use the built-in combineReducers.
The sub-reducers and call to combineReducers are as follows:
const todo = (state = {} ,action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id, text: action.text, completed: false,
};
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state;
}
return {
...state, completed: !state.completed,
};
default:
return state;
}
}
const todos = (state=[], action) =>{
switch (action.type) {
case 'ADD_TODO':
console.log('ADD_TODO switch selected')
return [
...state,
todo(undefined,action),
];
case 'TOGGLE_TODO':
console.log('TOGGLE_TODO switch selected')
return state.map( t => todo(t, action))
default:
console.log('default switch selected')
return state;
}
}
const visibilityFilter = (
state = 'SHOW_ALL',
action
) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
}
const todoApp = combineReducers({
todos,
visibilityFilter,
})
My questions are:
What was it in my code that caused this nesting of one reducer within the other?
I realize Dan used reduce instead, but for pedagogical reasons, how can I go about using the for ... in pattern to implement combineReducers?
After that, can you please comment on the appropriateness of using for ... in for such applications, and if it is a bad pattern, what is it that makes it so?
I just realized that the todos reducer and visibilityFilter reducers have to be passed the part of the combined state that corresponds to their key, not the entire combined state. So the working code should look like this, where I have added an object accessor to the corresponding part of the state in the 5th line.
const combineReducers = (reducers) => {
return (state,action) => {
let temp = {};
for (let i in reducers) {
temp[i] = reducers[i](state[i],action)
}
return temp;
}
}
Related
I found this tutorial https://www.digitalocean.com/community/tutorials/react-crud-context-hooks that describes how to build CRUD app with react hooks and context API.
Provided that state is described as array with object and in appReducer we have operations:
create
edit
remove
is it possible to have also a get method that would return a single object if id is matched with payload.id?
const initialState = {
products: [
{
id: 1,
name: "Milk",
amount: 3,
company: "XXX"
}
]
};
export default function appReducer(state, action) {
switch (action.type) {
case "ADD_PRODUCT":
return {
...state,
products: [...state.product, action.payload],
};
case "EDIT_PRODUCT":
const updatedProduct = action.payload;
const updatedProducts = state.products.map((prod) => {
if (product.id === updatedProduct.id) {
return updatedProduct;
}
return prod;
});
return {
...state,
products: updatedProducts,
};
case "REMOVE_PRODUCT":
return {
...state,
products: state.products.filter(
(prod) => prod.id !== action.payload
),
};
default:
return state;
}
};
What came to my mind is adding selectedProduct property in initialState however I everytime it was required to return products array as well.
Also I was thinking about adding additional reducer however, I have a synchronization problem with this solution and selectedProduct is returned is out of date.
Based on my understanding of the question I believe you are trying to fetch a single product from the list. Well you can add a case for that in your reducer like 'GET_SINGLE_PRODUCT'.
export default function appReducer(state, action) {
switch (action.type) {
case "GET_SINGLE_PRODUCT":
return {
...state,
product: state.products.find(
(prod) => prod.id === action.payload
),
};
default:
return state;
}
};
In State
const initialState = {
products: [
{
id: 1,
name: "Milk",
amount: 3,
company: "XXX"
}
],
product: {}
};
I'm going to add an object to the array, the second time I want to add another object the whole array becomes number one and I end up with an error, my goal is to add a task to program Todo with Redux.
I also get this errors:
TypeError: Cannot read property 'length' of undefined
TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
//todoReducer.js
import {ADD_TODO} from '../Actions/Todo';
const initialState = {
todos:[],
};
const handleAddTodo = (state, action) => {
const {todos} = state;
const newTodo =[...todos, {
id: todos.length + 1,
text: action.title,
isComplete: false,
}]
return (
todos.push(newTodo)
)
}
export default function todoRDS(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return handleAddTodo(state, action)
default:
return state
}
}
Change your return function you return wrong value. You need to return the state
const handleAddTodo = (state, action) => {
const {todos} = state;
return {
...state,
todos: [...todos, {
id: todos.length + 1,
text: action.title,
isComplete: false,
}]
}
}
export default function todoRDS(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return {...state, todos: [...state.todos, { id: state.todos.length +1, title: action.title, isComplete: false }] }
default:
return state
}
}
state is unmutable in react and redux you need to create a new state with old state values and add your new todo inside that new object. If you still want to use handeAddTodo try this:
const handleAddTodo = (state, action) => {
return {...state, todos: [...state.todos, { id: state.todos.length +1, title: action.title, isComplete: false }] }
}
Let's say I have normalized state like this (using normalizr)
entities: {
todos: {
'aaa': {
id: 'aaa',
text: 'Todo A',
completed: false
},
'bbb': {
id: 'bbb',
text: 'Todo B',
completed: false
},
'ccc': {
id: 'ccc',
text: 'Todo C',
completed: false
}
}
}
Then I have an action that fetches array of IDs from the server, that are completed. For that action type, I have reducer like this:
const todos = (state = initialState, action) => {
switch (action.type) {
case ActionTypes.FETCH_COMPLETED_SUCCESS:
return {
...state,
todos: action.payload.completedIds.map((id) => {
return {
[id]: {
...state.todos[id],
completed: true
}
}
})
};
default:
return state;
}
};
If I recieved an array with ['aaa', 'ccc'] (could be thousands of items in real world app), I want to set "completed" to TRUE on those respective todos in a single action, is it possible?
My current implementation of reducer doesn't work since it returns an array of objects, while original normalized state is object with IDs as key.
Thanks.
You could store the modified todos in an object and then update it using the spread syntax like
const todos = (state = initialState, action) => {
switch (action.type) {
case ActionTypes.FETCH_COMPLETED_SUCCESS:
let newTodos = {};
action.payload.completedIds.forEach((id) => {
newTodos[id]: {
...state.todos[id],
completed: true
}
})
})
return {
...state,
todos: {
...state.todos,
...newTodos
}
};
default:
return state;
}
};
I think this is the solution for your problem:
const todos = (state = initialState, action) => {
switch (action.type) {
case ActionTypes.FETCH_COMPLETED_SUCCESS:
return {
...state,
todos: Object.keys(action.payload.completedIds).reduce((previous, current) => {
previous[current] = {
...state.todos[current],
completed: true
}
return previous;
}, {})
})
};
default:
return state;
}
};
I used this solution because returns a new object and leaves the original object as it is
You can try this:
const todos = (state = initialState, action) => {
switch (action.type) {
case ActionTypes.FETCH_COMPLETED_SUCCESS:
const newState = {};//New Object
const filterKeys = Object.keys(state).filter( key=>{
newState[key]=state[key]; //copy
return completedIds.indexOf(key)!=-1;
});
filterKeys.forEach(key=> newState[key].completed = true);
return newState;
default:
return state;
}
};
I'm fairly new to react/redux and I've encountered a problem: my connect() component doesn't trigger render(). After some debugging I was pretty surprised to see that the root of this issue lies inside of a reducer. Apparently, after my action is dispatched, the reducer somehow gets the updated state inside of it's first argument (although it should be the old state, according to the docs), and that is why connect() component can't receive new props. Here is my reducer:
import * as types from '../actions/action-types';
const chart = (state = {}, action) => {
switch (action.type) {
case types.ADD_CHART:
let series = [
{
id: 0,
visible: true,
title: action.series[0]
}
];
for (let i = 1; i < action.series.length; i++) {
series.push({
id: i,
visible: false,
title: action.series[i]
});
}
return {
id: action.id,
series
}
case types.UPDATE_CHART:
if (state.id !== action.id) {
return state;
}
let result = {
id: action.id,
series: action.series
};
return {
id: action.id,
series: action.series
};
default:
return state;
}
}
const charts = (state = [], action) => {
console.log('charts reducer:');
console.log('old state:'); // it outputs the updated state
console.log(state);
switch (action.type) {
case types.ADD_CHART:
return [
...state,
chart(undefined, action)
];
case types.UPDATE_CHART:
let newState = state.map(c => chart(c, action));
console.log('new state:');
console.log(newState);
return state.map(c => chart(c, action));
default:
return state;
}
}
export default charts;
Any thoughts on this?
I'm a newbie in redux and es6 syntax. Here the problem:
There is an app with multiple posts.
const initialState = {
items: {
3: {title: '1984'},
6: {title: 'Mouse'},
19:{title: 'War and peace'}
}
}
App receive an array of liked posts ids:
dispatch(receiveLikedPosts(3, {id:3, ids: [3,6]}));
function receiveLikedPosts(ids) {
return {
type: LIKED_POSTS_RECEIVED,
ids
};
}
There is a posts reducer:
function posts(state = initialState, action) {
switch (action.type) {
case LIKED_POSTS_RECEIVED:
// here I need to update my posts state: post.liked => true (only 3 and 6 post)
default:
return state;
}
}
1) I have to update my reducers LIKED_POSTS_RECEIVED code. Dunno how to make it in right way.
2) Is it correct to dispatch events multiple times? ( one dispatch for each liked post)
Here the code:
// action
let ids = [3,6]
for (let id of ids) {
dispatch({type: LIKE, id});
}
// reducers
function post(state, action) {
switch (action.type) {
case LIKE:
return Object.assign({}, state, {
liked: true
});
default:
return state;
}
}
function posts(state = initialState, action) {
switch (action.type) {
case LIKE:
return Object.assign({}, state, {
[action.id]: post(state[action.id], action)
});
default:
return state;
}
}
This is confusing to me:
dispatch(receiveLikedPosts(3, {id:3, ids: [3,6]}));
function receiveLikedPosts(ids) {
return {
type: LIKED_POSTS_RECEIVED,
ids
};
}
Your function receiveLikedPosts only accepts one argument, yet you're passing it two. And I'm not sure what { id: 3, ids: [3, 6] } is supposed to be doing. But, here's what I would do:
Initial state and reducer:
const initialState = {
items: {
3: { title: '1984', liked: false },
6: { title: 'Mouse', liked: false },
19: { title: 'War and peace', liked: false }
}
};
function posts(state = initialState, action) {
switch (action.type) {
let newItems = {};
case LIKED_POSTS_RECEIVED:
// copy the current items into newItems
newItems = {...state.items};
// Loop through the liked IDs, set them to liked:true
action.ids.forEach((likedId) => {
newItems[likedId].liked = true;
});
// Return the new state
return {
...state,
items: newItems,
}
default:
return state;
}
}
Action creator:
function receiveLikedPosts(ids) {
return {
type: LIKED_POSTS_RECEIVED,
ids,
};
}
And finally, the dispatch:
dispatch(receiveLikedPosts([3, 6]));