Hi everyone I have some redux-toolkit issue that I believe comes from immer but cannot be sure.
Using createSlice I am creating reducer to manage open/close/move/add of UI tabs. all of the reducers are working properly except the closeTab reducer.
This is an example of the state at the moment of execution of closeTab reducer:
[{
id: 1,
active: false,
tittle: 'Cute',
},
{
id: 2,
active: true,
tittle: 'Cute',
}]
This is the entire createSlice
export const tabsSlice = createSlice({
name: "tabs",
initialState,
reducers: {
moveTab: (tabs, action: PayloadAction<{ dragIndex: number, hoverIndex: number }>) => {
tabs.splice(action.payload.hoverIndex, 0, tabs.splice(action.payload.dragIndex, 1)[0]);
},
selectTab: (tabs, action: PayloadAction<number>) => {
tabs.forEach(tab => tab.active = tab.id === action.payload);
},
closeTab: (tabs, action: PayloadAction<number>) => {
const isCurrentActive = tabs[action.payload].active;
tabs.splice(action.payload, 1);
if (isCurrentActive && tabs.length !== 0) {
const newActive = action.payload === 0 ? 0 : action.payload - 1;
tabs[newActive].active = true;
}
if (tabs.length === 0) {
tabs.push({
id: Date.now(),
active: true,
tittle: 'Cute2'
})
}
},
addTab: (tabs) => {
tabs.forEach(tab => tab.active = false)
tabs.push({
id: Date.now(),
active: true,
tittle: 'Cute2'
})
},
}
})
As mentioned moveTab, selectTab and addTab work perfectly but when closeTab is executed,
the array is spliced (the tab is removed), but the active property of the state is not changed. And I am sure that at the end of the reducer the state is as I want it.
State should be changed from:
[{
id: 1,
active: false,
tittle: 'Cute',
},
{
id: 2,
active: true,
tittle: 'Cute',
}]
to
[{
id: 1,
active: true,
tittle: 'Cute',
}]
But in the component I am receiving this:
[{
id: 1,
active: false,
tittle: 'Cute',
}]
The array length is changed, but not the active property
false issue selectTab overriding closeTab
Related
I have a Flatlist of about 20 filters.
Each filter selector is a Pressable.
when either filters with id 11 or 12 is checked, filter id 14 needs to become disabled.
If filter 14 is checked, both filters 11 & 12 need to become disabled.
I am handling the state with redux.
Summary - The function is doing what it is supposed to do and the Redux state is being updated correctly. The Flatlist is getting re-rendered but the disabled state on the UI doesn't change.
This is the filters arrays -
export const area = [
{ id: 1, txt: "West shore South", isChecked: false, isDisabled: false },
{ id: 2, txt: "West shore North", isChecked: false, isDisabled: false },
{ id: 3, txt: "North shore", isChecked: false, isDisabled: false },
{ id: 4, txt: "East shore", isChecked: false, isDisabled: false },
{ id: 5, txt: "South shore", isChecked: false, isDisabled: false },
{ id: 6, txt: "Barbuda", isChecked: false, isDisabled: false },
{ id: 7, txt: "Outer Islands", isChecked: false, isDisabled: false },
];
export const access = [
{ id: 11, txt: "Car", isChecked: false, isDisabled: false },
{ id: 12, txt: "Hike", isChecked: false, isDisabled: false },
{ id: 13, txt: "Public", isChecked: false, isDisabled: false },
{ id: 14, txt: "Boat Only", isChecked: false, isDisabled: false },
];
(There are couple of extra filter arrays that are irrelevant to this question).
This is the Redux slice that handles the state -
import { createSlice } from "#reduxjs/toolkit";
import { area, access, nature, other } from "../logic/FiltersData";
export const activeFiltersSlice = createSlice({
name: "activeFilters",
initialState: {
filters: [...area, ...access, ...other, ...nature],
active: [],
},
reducers: {
updateAllFilters: (state, action) => {
state.filters = action.payload;
state.active = state.filters.filter((each) => each.isChecked);
},
},
});
export const { updateAllFilters } = activeFiltersSlice.actions;
export default activeFiltersSlice.reducer;
This is the function I used to handle the state before trying to disable buttons (products being the array of all the filters)
const handleChange = (id) => {
let temp = products.map((product) => {
if (id === product.id) {
return { ...product, isChecked: !product.isChecked };
}
return product;
});
dispatch(updateAllFilters(temp));
};
This is the same function after I added the disabled change of state.
const handleChange = (id) => {
const car = products.findIndex((item) => item.id === 11);
const hike = products.findIndex((item) => item.id === 12);
let temp = products.map((product) => {
if (id === 11 || id === 12) {
if (product.id === 14) {
if (
(id === 11 &&
products[car].isChecked &&
!products[hike].isChecked) ||
(id === 12 && !products[car].isChecked && products[hike].isChecked)
) {
return {
...product,
isDisabled: false,
};
} else {
return {
...product,
isDisabled: true,
};
}
} else if (id === product.id) {
return { ...product, isChecked: !product.isChecked };
}
}
if (id === 14) {
if (product.id === 11 || product.id === 12) {
return {
...product,
isDisabled: !product.isDisabled,
};
} else if (id === product.id) {
return { ...product, isChecked: !product.isChecked };
}
}
if (id === product.id) {
return { ...product, isChecked: !product.isChecked };
}
return product;
});
dispatch(updateAllFilters(temp));
};
This is how I render the Flatlist (Section being one array of filters)
const renderFlatList = (section) => {
const print = section.map((item, index) => {
const found = products.find(
(element) => element.id === item.id
).isChecked;
return (
<Pressable
key={item.id}
style={found ? styles.itemChecked : styles.item}
hitSlop={5}
onPress={() => handleChange(item.id)}
disabled={item.isDisabled}
>
<View style={styles.itemText}>
<Text style={found ? styles.txtChecked : styles.txt}>
{item.txt}
</Text>
</View>
<View style={styles.icon}>
<MaterialCommunityIcons
name={
found
? "checkbox-multiple-marked-circle"
: "checkbox-multiple-blank-circle-outline"
}
size={32}
color={iconColor}
/>
</View>
</Pressable>
);
});
return print;
};
I use this code to verify the Redux state is updating correctly after each click, which it does
products.map((each) => {
if (each.id > 10 && each.id < 20) {
console.log(each);
}
});
However, the filters that showing on the Redux as isDisabled: true are not becoming disabled!
Why?
Thanks
And a secondary question - if anyone have an idea how to write the handleChange() function cleaner and more efficient, that will be lovely.
I'm making a todo app and using redux for state management. My todo state is made up of nested arrays.
const initialState = {
todos: [
{
id: 1,
name: "task1",
subdata: [
{
id: 101,
name: "subtask1",
complete: false,
},
{
id: 102,
name: "subtask2",
complete: true,
},
],
},
{
id: 2,
name: "task2",
subdata: [
{
id: 103,
name: "subtask3",
complete: false,
},
{
id: 104,
name: "subtask4",
complete: true,
},
],
},
Reducers:
export default function reducer(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
const newTodo = state.todos.concat(action.payload);
return { ...state, todos: newTodo };
case ADD_SUBTODO:
const newSubtodo = action.payload;
?????????????????????????????????????????????
How can i append new subtodo to initialstate?
I used the immer library, but I want to do it the traditional way, for example the spread operator. I would be glad if you help.
You could do something like...
// send payload as {id:1,newSubtodo: newSubtodo}
case ADD_SUBTODO:
const newSubtodo = action.payload.newSubtodo;
//id is the task/todo id of which you want to add a new subdata
const newTask = initialState.todos.find(i=>i.id==action.payload.id)
//id is the task/todo id of which you want to add a new subdata
newTask.subdata.push(newSubtodo)
return {...initialState,todos:[...initialState.todos,newTask]}
Note: Using nested objects as state in React Js is not a good
practice.
const CategoriesData = [
{
name: "Category1",
isActive: true,
children: [
{
name: "Category1Child",
isActive: false,
}
]
},
{
name: "Category2",
isActive: false,
},
{
name: "Category3",
isActive: true,
children: [
{
name: "Category3Child",
isActive: false,
}
]
}
];
const [disabledCategories, setDisabledCategories] = useState([]);
function notActiveCategories(categories) {
// Loop logs out at least 7 isActive: false categories.
categories.forEach((category) => {
if (category.isActive) notActiveCategories(category.children);
if (!category.isActive) {
setDisabledCategories([...disabledCategories, category]);
console.log(category);
}
});
};
useEffect(() => {
notActiveCategories(CategoriesData);
console.log(disabledCategories); // Only 1 category is in the array.
}, []);
I feel like the function the loop is in calling itself is causing the disabledCategories state to revert to when it was empty and that is leading to only the last step of the foreach to be set.
So how would i get this to loop through the categories array and have the disabledCategories state to contain all of the category objects that have isActive: false.
Which in the example of CategoriesData above, it would mean that the disabledCategories state would contain:
[
{
name: "Category1Child",
isActive: false,
},
{
name: "Category2",
isActive: false,
},
{
name: "Category3Child",
isActive: false,
},
];
Try changing your setDisabledCategories to use the previous state param that comes from setState:
setDisabledCategories(prevState => [...prevState, category])
When multiple setState calls are batched together you need to be careful so they don't override each other. Using this method ensures that your setState calls are "chained" so you always get the updated state.
Way 1: Affect after recursive loop
function notActiveCategoriesRecusive(categories) {
let notActive = []
categories.forEach((category) => {
if (category.isActive) notActive = [...notActive, ...(notActiveCategories(category.children))];
if (!category.isActive) {
notActive.push(category)
}
});
return notActive
};
function notActiveCategories(categories) {
setDisabledCategories(notActiveCategoriesRecusive(categories)
}
Way 2: Get the last state because it doesn't has time to refresh
function notActiveCategories(categories) {
categories.forEach((category) => {
if (category.isActive) notActiveCategories(category.children);
if (!category.isActive) {
setDisabledCategories(oldState => ([...oldState, category]))
}
});
};
I'd only call setState once with the filtered array:
const findInactive = data =>
data.filter(e => !e.isActive)
.concat(...data.filter(e => e.children)
.map(e => findInactive(e.children)))
;
const categoriesData = [ { name: "Category1", isActive: true, children: [ { name: "Category1Child", isActive: false, } ] }, { name: "Category2", isActive: false, }, { name: "Category3", isActive: true, children: [ { name: "Category3Child", isActive: false, } ] } ];
const inactive = findInactive(categoriesData)
// the following is neeeded if it's possible for a
// node to have children and be inactive
// .map(({name, isActive}) => ({name, isActive}))
;
console.log(inactive);
//setDisabledCategories(inactive); // one time in React
This makes the code a lot easier to reason about and decouples React's API out from the filtering logic, which can be moved out to a generic function agnostic of React.
As others have mentioned, if you do want to call setState multiple times as a batch update, you can use the prevState callback to chain the updates: setDisabledCategories(prevState => [...prevState, category]);.
Hi I am using immutableJS and I would want to update multiple objects in my array if it has the same id from action.contacts
const initialState = fromJS({
list: [{
id: 1,
loading: false,
}, {
id: 2,
loading: false,
}, {
id: 3,
loading: false,
}]
});
action.contacts = [{
id: 1
}, {
id: 2
}]
I expected when I call state.get('list') it would equal to
list: [{
id: 1,
loading: true,
}, {
id: 2,
loading: true,
}, {
id: 3,
loading: false,
}]
what I have done so far is this:
case UNLOCK_CONTACTS:
const indexesOfRow = state.get('list').findIndex((listItem) => {
return action.contacts.map((contact)=> listItem.get('id') === contact.id)
})
return indexesOfRow.map((index)=> {
state.setIn(['list', index, 'loading'], true);
});
}));
but it's not working out for me, didn't update anything
I created a similar solution in a fiddle http://jsfiddle.net/xxryan1234/djj6u8xL/398/
You are missing the point of immutable.js. The objects are not mutable.
const initialState = Immutable.fromJS({
list: [{
id: 1,
loading: false
}, {
id: 2,
loading: false
}, {
id: 3,
loading: false
}],
});
const contacts = [{
id: 1
}, {
id: 3
}]
let newState = initialState.set( 'list', initialState.get('list').map( (row,index) => {
let contact = contacts.find((item)=>{
return item.id == row.get('id')
})
if (contact){
return row.set('loading', true)
}
return row;
}))
console.log(newState.toJS())
see in the updated fiddle http://jsfiddle.net/djj6u8xL/399/
const newState = initialState.update('list', (oldValue) =>
oldValue.map(item =>
action.contacts.find(act =>
act.get('id') === item.get('id')) !== undefined ?
item.update('loading',(oldVal)=> !oldVal) : item))
console.log(newState.toJS())
notice: you need to turn action.contacts into immutable list of immutable Maps.
case UNLOCK_CONTACTS:
return state.set('list', state.get('list').map((listItem) => {
const matched = _.find(action.contacts, (contact) => listItem.get('id') === contact.id);
if (matched) {
return fromJS({ ...listItem.toJS(), loading: true });
}
return listItem;
}));
So I manage to solve it by mapping the list and then finding if the listItem exists in action.contacts. If it matches I return the matched object with loading: true and if not I return the same object.
I am open to suggestions how can I refactor this solution though, I am quite new to immutable js and I feel there is much more simpler way to solve this but I don't know yet.
I would like to change the active for item 1 to true, while also setting others (some hundred) active to false.
import {Map, Record} from 'immutable';
const initialState = new Map({
0: new Record({
id: 0,
name: 'foo',
active: true,
}),
1: new Record({
id: 1,
name: 'bar',
active: false,
}),
2: new Record({
id: 2,
name: 'baz',
active: false,
}),
});
const TARGET = 1;
const newState = initialState.merge(
initialState.map((value) => {
if (value.id === TARGET) {
return value.set('active', true);
} else if (value.active) {
return value.set('active', false);
}
return value;
})
);
Is it possible to do it easiest or better? Thanks!