Small Background intro
I´m currently working the User Administration page of my project and running into a small problem here. I have a table which contains some material-ui`s Usercard. For Each user that uses my System exist´s one card. The card´s are generated from data that comes from my database and then written into a redux store.
The Admin can do several interactions with the database that changes some Userdata. To provide an easy way to find a specific user a <TextField /> was implemented that filter´s the table of Usercards.
All of the things mentioned here works!
The Problem
As mentioned in the Intro the data are stored in a redux store. When I filter the data, an action is dispatched
export const FILTER_ALL_USER_BY_NAME = "FILTER_ALL_USER_BY_NAME"
export const FILTER_ALL_USER_BY_DEPARTMENT = "FILTER_ALL_USER_BY_DEPARTMENT"
export default function filterAllUser(filter, filterOption){
return (dispatch, getState) => {
if(filterOption === 'name'){
return dispatch (filterUserByName(filter))
}else{
return dispatch (filteUserByDepartment(filter))
}
}
}
function filterUserByName(filter){
return {
type: FILTER_ALL_USER_BY_NAME,
filter: filter
}
}
function filteUserByDepartment(filter){
return {
type: FILTER_ALL_USER_BY_DEPARTMENT,
filter: filter
}
}
The Reducer
Even if the reducers works, it is the main reason for my problem.
Why?
It is because, when I filter the data I was not able to really filter the state, rather then return a new state object which leads me to the problem that the allUserData and filteredUserData get out of sync after the userdata are changed.
Let me explain this in code
function allUser(state = {allUserData: []}, action){
switch (action.type){
case 'REQUEST_ALL_USER':
return Object.assign({}, state, {
isFetching: true
});
case 'RECEIVE_ALL_USER':
return Object.assign({}, state, {
isFetching: false,
allUserData: action.items
});
case 'FILTER_ALL_USER_BY_NAME':
return Object.assign({}, state, {
filteredUserData: state.allUserData.filter(user => user.userNameLast.toLowerCase().indexOf(action.filter.toLowerCase()) >= 0)
});
case 'FILTER_ALL_USER_BY_DEPARTMENT':
return Object.assign({}, state, {
filteredUserData: state.allUserData.filter(user => user.departmentName.toLowerCase().indexOf(action.filter.toLowerCase()) >= 0)
});
default:
return state
}
}
But when I´m trying to filter the original state and the user removes the filter, the data that didn´t matched the filter are gone.
case 'FILTER_ALL_USERS': return allUsers.filter(user => user.userNameLast.toLowerCase().indexOf(action.filter.toLowerCase()) >= 0);
How can I filter the state, but keep the data ?
As requested, I have put together some code for you. It would be something like below.
As a side note, I would pass the filter as { field: 'userLastName', text: your filter text} as the filter criteria. Or to make it even more scalable, you can pass a filter-handler instead of text above. That way you can have any type of filter, and not just text filters.
export default function allUser(state = {allUserData: [], filters: {}, filteredUserData: []}, action){
switch (action.type){
case 'REQUEST_ALL_USER':
return {
...state,
isFetching: true
};
case 'RECEIVE_ALL_USER':
return {
...state,
isFetching: false,
allUserData: action.items,
filteredUserData: filterData(action.items, state.filters),
};
case 'FILTER_ALL_USER_BY_NAME':
{
const updatedFilters = {
...state.filters,
userNameLast: action.filter
}
return {
...state,
filteredUserData: filterData(state.allUserData, updatedFilters),
filters: updatedFilters
};
}
case 'FILTER_ALL_USER_BY_DEPARTMENT':
{
const updatedFilters = {
...state.filters,
departmentName: action.filter
}
return {
...state,
filteredUserData: filterData(state.allUserData, updatedFilters),
filters: updatedFilters
};
}
default:
return state
}
}
const filterData = (users, filters) => {
return users.filter(filterFn(filters));
};
const filterFn = filters => item => Object.keys(filters).reduce((res, filter) => {
return res && item[filter].toLowerCase().indexOf(filters[filter].toLowerCase()) >= 0;
}, true);
Unit tests
import usersReducer from './users';
describe('usersReducer', () => {
describe('RECEIVE_ALL_USER', () => {
const RECEIVE_ALL_USER = 'RECEIVE_ALL_USER';
it('should replace users in state', () => {
const initialState = { isFetching: false, allUserData: [{ name: 'A' }], filters: {}};
const newUsers = [{ name: 'B' }];
const newState = { ...initialState, allUserData: newUsers, filteredUserData: newUsers};
expect(usersReducer(initialState, { type: RECEIVE_ALL_USER, items: newUsers })).toEqual(newState);
})
})
describe('FILTER_ALL_USER_BY_NAME', () => {
let FILTER_ALL_USER_BY_NAME = 'FILTER_ALL_USER_BY_NAME';
it('should filter users by name', () => {
const initialState = { isFetching: false, allUserData: [{ userNameLast: 'Doe' }, { userNameLast: 'Smith' }], filters: {}};
const filterText = 'd';
const finalState = { isFetching: false,
allUserData: [{ userNameLast: 'Doe' }, { userNameLast: 'Smith' }],
filters: { userNameLast: filterText },
filteredUserData: [{ userNameLast: 'Doe' }]
};
expect(usersReducer(initialState, { type: FILTER_ALL_USER_BY_NAME, filter: filterText})).toEqual(finalState);
})
})
describe('FILTER_ALL_USER_BY_DEPARTMENT', () => {
let FILTER_ALL_USER_BY_DEPARTMENT = 'FILTER_ALL_USER_BY_DEPARTMENT';
it('should filter users by department', () => {
const initialState = { isFetching: false, allUserData: [{ departmentName: 'IT' }, { departmentName: 'Human Resources' }], filters: {}};
const filterText = 'it';
const finalState = {
...initialState,
filters: { departmentName: filterText },
filteredUserData: [{ departmentName: 'IT' }]
};
expect(usersReducer(initialState, { type: FILTER_ALL_USER_BY_DEPARTMENT, filter: filterText})).toEqual(finalState);
})
})
});
Best for these cases is to some selector library, example reselect. Instead of editing the original state create selectors for sorting and filter and pass the result to component.
There's also a quite similar example in reselect documentation https://github.com/reactjs/reselect#selectorstodoselectorsjs.
Related
I have a data structure that is like this:
const dataObj = {
myparam: 'Hello',
data: [
{
id: 1, checked: false
},
{
id: 2, checked: true
}
{
id: 3, checked: false
}
]
}
I have been experimenting with useReducer since I wanted to update my arrayList so that when I send in my payload I could change the object with id 1 to be checked/unchecked.
I solved this by doing this:
const reducer = (state: StateType, action: Action) => {
const { type, payload } = action;
let newArrayChecked: Array<DataItems> = [];
let newArrayUnchecked: Array<DataItems> = [];
switch (type) {
case ActionKind.Checked:
newArrayChecked = state.items.map((item) => {
const it = item;
if (item.id === payload.id) it.checked = true;
return it;
});
return {
...state,
items: newArrayChecked,
};
case ActionKind.UnChecked:
newArrayUnchecked = state.items.map((item) => {
const it = item;
if (item.id === payload.id) it.checked = false;
return it;
});
return {
...state,
items: newArrayUnchecked,
};
default:
return state;
}
};
Im not so happy about this since for starters its repetative code more or less and looks ugly.
Im wondering if there is a better way to do this with useReducer? Im fairly new to this Hook and looking for code optimazation.
You just update like this:
case ActionKind.Checked:
newArrayChecked = state.items.map((item) => {
return {
...item,
checked: item.id === payload.id ? true: item.checked
};
});
return {
...state,
items: newArrayChecked,
};
case ActionKind.UnChecked:
newArrayChecked = state.items.map((item) => {
return {
...item,
checked: iitem.id === payload.id ? false : item.checked
};
});
return {
...state,
items: newArrayUnchecked,
};
I'm trying to do a delete todo and I want to remove the item from the object "byIds" with the specific id.
It will be like a filter for arrays but for the object.
I don't know what's so complicated hope for help I believe its stupid
import { ADD_TODO, TOGGLE_TODO, DELETE_TODO } from "../actionTypes";
const initialState = {
allIds: [],
byIds: {},
};
export default function (state = initialState, action) {
switch (action.type) {
case ADD_TODO: {
const { id, content } = action.payload;
return {
...state,
allIds: [...state.allIds, id],
byIds: {
...state.byIds,
[id]: {
content,
completed: false,
},
},
};
}
case TOGGLE_TODO: {
const { id } = action.payload;
return {
...state,
byIds: {
...state.byIds,
[id]: {
...state.byIds[id],
completed: !state.byIds[id].completed,
},
},
};
}
// of course its toggling but it doesn't even get there
case DELETE_TODO: {
const { id } = action.payload;
return {
...state,
allIds: state.allIds.filter((todo) => todo !== id),
byIds: state.byIds.filter((todo) => todo !== id),
};
}
default:
return state;
}
}
{
todos: {
allIds: [
1,
2,
3,
4
],
byIds: {
'1': {
content: 'Test1',
completed: false
},
'2': {
content: 'Test2',
completed: false
},
'3': {
content: 'test3',
completed: false
},
'4': {
content: 'test4',
completed: false
}
}
},
visibilityFilter: 'all'
}
That for the one who asked me to console log the byIds hope that will help me
What you need is to iterate through the keys of byids object and take only the ones you need.
case DELETE_TODO: {
const { id } = action.payload;
let newObj = {}
Object.keys(state.byIds).forEach(function(key) {
if (key !== id) {
newObj[key] = state.byIds[key]
}
});
return {
...state,
allIds: state.allIds.filter((todo) => todo !== id),
byIds: newObj
};
}
In case your id is not a string but a number, you need to check with key != id and not key !== id
You could use the rest syntax to exclude the id when creating the new object.
case DELETE_TODO: {
const { id } = action.payload;
const { [id]: _, ...restByIds } = state.byIds;
return {
...state,
allIds: state.allIds.filter((todo) => todo !== id),
byIds: restByIds,
};
}
You can separate the id out like this:
case DELETE_TODO: {
const id = action.payload.id;
const { [id]: _, ...filteredState } = state.byIds;
// ^ this takes id from state and puts it in variable _,
// everything else is packed into filteredState
return {
...state,
allIds: state.allIds.filter((todo) => todo !== id),
byIds: filteredState,
};
}
Edit:
Extra notes for anyone wondering about the syntax above, see these links:
Computed Property Names, how we are grabbing [id]
Destructuring assignment, how we are packing back into ...filteredState
Awesome comment explaining destructuring
My data looks like this:
{
'004': [
{
year_week: '2020-W1',
actual_bank_amount: '6500000',
ext_in_rental_income: '',
ext_in_tax_refund: '',
ext_in_dividends_income: ''
},
{
year_week: '2020-W2',
actual_bank_amount: '6500000',
ext_in_rental_income: '',
ext_in_tax_refund: '',
ext_in_dividends_income: ''
}
],
'007': [
{
year_week: '2020-W22',
actual_bank_amount: '65050000',
ext_in_rental_income: '30000',
ext_in_tax_refund: '',
ext_in_dividends_income: ''
}
]
},
I am trying to update say date for year_week '2020-W1' in '004'.
No problem with action and reducer but data is not updated in the list.
Below is my reducer:
case 'UPDATE':
state.planningData[action.payload.currentSite].map((item, index) => {
if (item.year_week === action.payload.data.year_week) {
return Object.assign({}, item, action.payload.data);
}
return item;
});
console.log(state)
return {
loading: true,
planningData: state.planningData,
error: ''
}
What I am doing wrong please. Btw when I do console log or run redux extension I see the updated state.
Below is my action creator:
export const update = (data) =>
(dispatch, getState) => {
console.log("Update action called" + JSON.stringify(data))
const currentSite = getState().sites.currentSite;
dispatch({
type: 'UPDATE',
payload: {
data: data,
currentSite: currentSite
}
});
};
btw I am calling it from a editable cell component on "enter" and blur event below is my code
const save = async e => {
try {
const values = await form.validateFields();
toggleEdit();
dispatch(update({ ...record, ...values }));
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};
This isn't pretty but it works. You had a bit of nested data in your state and it wasn't being updated properly.
case "UPDATE":
let updatedPlanningData = {};
for (let prop in state.planningData) {
if (prop === action.payload.currentSite) {
updatedPlanningData[action.payload.currentSite] = state.planningData[
action.payload.currentSite
].map((item, index) => {
if (item["year_week"] === action.payload.data.year_week) {
return Object.assign({}, item, action.payload.data);
}
return item;
});
} else {
updatedPlanningData.prop = state.planningData[prop];
}
}
return {
loading: true,
planningData: updatedPlanningData,
error: ""
};
Here is example code in codesandbox
Edit: more compact solution
let updatedPlanningData = {...state.planningData};
updatedPlanningData[action.payload.currentSite].map((item, index) => {
if (item["year_week"] === action.payload.data.year_week) {
return Object.assign(item, action.payload.data);
}
return item;
});
I'm making an application that gives tasks to learning methods. One of the reducers should change the state of the task mark: pass true or false depending on the solution of the task. But this code doesn't change the state of reducer.
My code:
const initialStateMethods = {
array: methodsObject
};
const methods = (state = initialStateMethods, action) => {
switch (action.type) {
case "CHANGE_MARK":
return {
...state,
array: state.array.map(method => {
if (method.id === action.methodIndex) {
method.tasks.map(task => {
if (task.id === action.taskIndex) {
return { ...task, mark: action.mark };
} else {
return task;
}
});
}
return method;
})
};
default:
return state;
}
};
But the value of the method changes easily and it works.
Example:
const methods = (state = initialStateMethods, action) => {
switch (action.type) {
case "CHANGE_MARK":
return {
...state,
array: state.array.map(method => {
if (method.id === action.methodIndex) {
return { ...method, name: "newName" };
}
return method;
})
};
default:
return state;
}
};
So I assume that the problem is in the multilayer structure
A small piece of the original object:
export const methodsObject = [
{
name: "from()",
id: 0,
tasks: [
{
taskName: "Task №1",
id: 0,
mark: null
},
{
taskName: "Task №2",
id: 1,
mark: null
}
]
}
You are missing a return statement next to the call of your map loop:
...
case "CHANGE_MARK":
return {
...state,
array: state.array.map(method => {
if (method.id === action.methodIndex) {
return method.tasks.map(task => {
if (task.id === action.taskIndex) {
return { ...task, mark: action.mark };
} else {
return task;
}
});
}
return method;
})
};
My store looks like this,
{
name: "john",
foo: {},
arr: [
{
id:101,
desc:'comment'
},
{
id:101,
desc:'comment2'
}
]
}
My textarea looks like this
<textarea
id={arr.id} //"101"
name={`tesc:`}
value={this.props.store.desc}
onChange={this.props.onChng}
/>
My action is
export const onChng = (desc) => ({
type: Constants.SET_DESC,
payload: {
desc
}
});
My reducer
case Constants.SET_DESC:
return update(state, {
store: {
streams: {
desc: { $set: action.payload.desc }
}
}
});
It works only if arry is an object, I had to make changes to the stream to an array and I am confused how I can update to an array, also how does get the right value from the store.
The following example taken from the redux documentation might help you in the use case how to update items in an array. For more on this you can read on here http://redux.js.org/docs/recipes/StructuringReducers.html
state structure is something like this
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
and reducer code is like below
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if(item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item;
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item);
return updatedItem;
});
return updatedItems;
}
function appReducer(state = initialState, action) {
switch(action.type) {
case 'EDIT_TODO' : {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, {text : action.text});
});
return updateObject(state, {todos : newTodos});
}
default : return state;
}
}
If you have to update an element in a array within your store you have to copy the array and clone the matching element to apply your changes.
So in the first step your action should contain either the already cloned (and changed) object or the id of the object and the properties to change.
Here is a rough example:
export class MyActions {
static readonly UPDATE_ITEM = 'My.Action.UPDATE_ITEM';
static updateItem(id: string, changedValues: any) {
return { type: MyActions.UPDATE_ITEM, payload: { id, changedValues } };
}
}
export const myReducer: Reducer<IAppState> = (state: IAppState = initialState, action: AnyAction): IAppState => {
switch (action.type) {
case MyActions.UPDATE_ITEM:
return { ...state, items: merge(state.items, action.payload) };
default:
return state;
}
}
const merge = (array, change) => {
// check if an item with the id already exists
const index = array.findIndex(item => item.id === change.id);
// copy the source array
array = [...array];
if(index >= 0) {
// clone and change the existing item
const existingItem = array[index];
array[index] = { ...existingItem, ...change.changedValues };
} else {
// add a new item to the array
array.push = { id: change.id, ...change.changedValues };
}
return array;
}
To update an array, I would use immutability helper and do something like this - to your reducer
let store = {"state" : {
"data": [{
"subset": [{
"id": 1
}, {
"id": 2
}]
}, {
"subset": [{
"id": 10
}, {
"id": 11
}, {
"id": 12
}]
}]
}}
case Constants.SET_DESC:
return update(store, {
"state" : {
"data": {
[action.indexToUpdate]: {
"subset": {
$set: action.payload.desc
}
}
}
}
})
});