I can't quite wrap my head around the boilerplate of redux. I looked up common patterns for immutable modifying of state but issue is, all these patterns simply push towards the end and not for a specific index.
Before I'll go into actual code, here's what the structure of the state looks like for better imagination (pseudo-code):
state = {
quizMenu: {...},
quizEditor: Array<Question>,
> type Question = {
id: number,
question: string,
questionOptions: Array<QuestionOption>,
}
> type QuestionOption = {
id: number,
optionText: string,
isValid: boolean,
}
}
Hopefully it makes sense. I have created an action for adding questions, which works fine. Now I'm trying to create an action for adding option to an already existing question, but I can't wrap my head around how to in the nested arrays of objects.
Here's how my action in question is defined:
const AQO = 'ADD_QUESTION_OPTION';
/*
* #param questionId - ID of the question we're accesssing in quizEditor array
* #param id - id of the option we're adding (handled in component)
*/
const actionAddQuestionOption = createAction(
AQO,
(questionId: number, id: number) => ({
payload: {
id,
optionText: 'New option',
isValid: false,
questionId,
},
})
);
Now my reducer is the following way:
const reducer = createReducer({//...}, {
[actionAddQuestionOption.type]: (state, action) => ({
...state,
quizEditor: [...state.quizEditor][action.payload.questionId].questionOptions.push({
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid,
})
})
}
This just ends up in this monster type-error: https://pastebin.com/raw/pBbnxcQp
But I'm pretty sure I'm accessing the Array inside the array of objects incorrectly.
quizEditor: [...state.quizEditor][action.payload.questionId].questionOptions
Does anyone know what would be the proper way of going about accessing it? Much appreciated!
Since you are using redux-toolkit which has immer built in you can just mutate the state directly and it will transform it into an immutable update internally
const reducer = createReducer({
[actionAddQuestionOption.type]: (state, { payload: { questionId, ...option }}) => {
const question = state.questionquizEditor(question => question.id === questionId)
question.questionOptions.push(option)
}
})
The way to make it an immuable update is like this
const reducer = createReducer({
[actionAddQuestionOption.type]: (state, { payload: { questionId, ...option } }) => ({
...state,
quizEditor: state.quizEditor.map(question =>
(question.id === questionId
? {
...question,
questionOptions: [...question.questionOptions, option],
}
: question)),
}),
})
the push method of Array returns the new length of the array not the array itself. What you can do is just concat the new object to the array which in turn will return the new array with the new question option.
[...state.quizEditor][action.payload.questionId].questionOptions.concat({
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid,
})
Furthermore, we have to modify only that property in the state with our new array:
const reducer = createReducer({
//...}, {
[actionAddQuestionOption.type]: (state, action) => {
const quizEditor = [...state.quizEditor];
quizEditor[action.payload.questionId].questionOptions = quizEditor[
action.payload.questionId
].questionOptions.concat({
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid
});
return {
...state,
quizEditor
};
}
});
Thanks to immer in redux toolkit we can make it more readable:
const reducer = createReducer({
//...}, {
[actionAddQuestionOption.type]: (state, action) => {
const question = state.quizEditor[action.payload.questionId];
question.questionOptions = [
...question.questionOptions,
{
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid
}
];
return state;
}
});
Related
I am trying to get some data back from my database and map over it to display in the client (using react/redux-toolkit).
The current structure of the data is a series of arrays filled with objects:
[
{
id: 2,
prize_id: 1,
book_id: 2,
author_id: 2,
}
]
[
{
id: 1,
prize_id: 1,
book_id: 1,
author_id: 1,
}
]
The front end is only displaying one of the arrays at a time despite needing to display both. I think the problem is in how my redux is set up. Becuase when I console.log the action.payload I get back just one of the arrays, usually the second one.
Here is how my redux slices and actions look:
slice:
const booksSlice = createSlice({
name: 'books',
initialState: {
booksByYear: [], //possibly the source of the problem
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(actions.fetchBooksByYear.fulfilled, (state, action) => {
console.log(action.payload)
state.booksByYear = action.payload
})
},
})
action:
export const fetchBooksByYear = createAsyncThunk(
'books/get books by prize and year year',
async ({ prizeId, prizeYear }) => {
const data = await api.getBooksByPrizeAndYear(prizeId, prizeYear.year)
return data
}
Here is how I am fetching the data from my component:
useEffect(() => {
dispatch(fetch.fetchPrizeYears(prizeId))
}, [dispatch])
const booksByYear = prizeYears.map((year, id) => {
console.log(year)
return <PrizeLists key={id} prizeYear={year} prizeId={prizeId} />
})
export default function PrizeLists(props) {
const dispatch = useDispatch()
const listOfBooks = useSelector((state) => state.books.booksByYear)
useEffect(() => {
dispatch(fetch.fetchBooksByYear(props))
}, [dispatch])
Previously it was working when the call
was being made without redux
So the booksByYear is expected to be an array of arrays, is that correct? For example:
booksByYear: [
[
{
id: 2,
prize_id: 1,
book_id: 2,
author_id: 2,
}
],
[
{
id: 1,
prize_id: 1,
book_id: 1,
author_id: 1,
}
]
]
The slice setup seems fine, I think the problem might be api.getBooksByPrizeAndYear.
Because the action.payload in the callback of builder.addCase is returned by the corresponding createAsyncThunk, which is fetchBooksByYear in your case.
So if action.payload is not something you're expecting, there's a high chance that the API is not responding the correct dataset in the first place.
I'm not sure about the use case in you app, if the API will only return one array at a time, you probably want to merge action.payload with state.booksByYear instead of replacing it.
Oh, now I know why you said initialState.booksByYear might be the problem! Yes, it is a problem because from you code it seems that you want to "group" those books by { prizeYear, prizeId }, and display the books in each group on UI. Due to the fact that there's only one array at the moment, the last fulfilled action will always overwrite the previous fulfilled action because of how we handle API response (state.booksByYear = action.payload).
In this case I think it makes more sense to leave those books in the component by using useState. But if you really want to store those books in redux, you could try making initialState.booksByYear into a Map-like object, and find the corresponding array by { prizeYear, prizeId } from <PrizeLists />.
For example:
// Slice
// You may want to implement the hash function that suits your case!
// This example may lead to a lot of collisions!
export const hashGroupKey = ({ prizeYear, prizeId }) => `${prizeYear}_${prizeId}`
const booksSlice = createSlice({
name: 'books',
initialState: {
// We can't use Map here because it's non serializable.
// key: hashGroupKey(...), value: Book[]
booksMap: {}
},
extraReducers: (builder) => {
builder.addCase(actions.fetchBooksByYear.fulfilled, (state, action) => {
const key = hashGroupKey(action.meta.arg)
state.booksMap[key] = action.payload
})
},
})
// PrizeLists
import { hashGroupKey } from '../somewhere/book.slice';
const listOfBooks = useSelector((state) => {
const key = hashGroupKey(props)
return state.books.booksMap[key] ?? []
})
I have the following code (I deleted most of it to make it easier to understand - but everything works):
"role" reducer:
// some async thunks
const rolesSlice = createSlice({
name: "role",
initialState,
reducers: { // some reducers here },
extraReducers: {
// a bunch of extraReducers
[deleteRole.fulfilled]: (state, { payload }) => {
state.roles = state.roles.filter((role) => role._id !== payload.id);
state.loading = false;
state.hasErrors = false;
},
},
});
export const rolesSelector = (state) => state.roles;
export default rolesSlice.reducer;
"scene" reducer:
// some async thunks
const scenesSlice = createSlice({
name: "scene",
initialState,
reducers: {},
extraReducers: {
[fetchScenes.fulfilled]: (state, { payload }) => {
state.scenes = payload.map((scene) => scene);
state.loading = false;
state.scenesFetched = true;
}
}
export const scenesSelector = (state) => state.scenes;
export default scenesSlice.reducer;
a component with a button and a handleDelete function:
// a react functional component
function handleDelete(role) {
// some confirmation code
dispatch(deleteRole(role._id));
}
My scene model (and store state) looks like this:
[
{
number: 1,
roles: [role1, role2]
},
{
number: 2,
roles: [role3, role5]
}
]
What I am trying to achieve is, when a role gets deleted, the state.scenes gets updated (map over the scenes and filter out every occurrence of the deleted role).
So my question is, how can I update the state without calling two different actions from my component (which is the recommended way for that?)
Thanks in advance!
You can use the extraReducers property of createSlice to respond to actions which are defined in another slice, or which are defined outside of the slice as they are here.
You want to iterate over every scene and remove the deleted role from the roles array. If you simply replace every single array with its filtered version that's the easiest to write. But it will cause unnecessary re-renders because some of the arrays that you are replacing are unchanged. Instead we can use .findIndex() and .splice(), similar to this example.
extraReducers: {
[fetchScenes.fulfilled]: (state, { payload }) => { ... }
[deleteRole.fulfilled]: (state, { payload }) => {
state.scenes.forEach( scene => {
// find the index of the deleted role id in an array of ids
const i = scene.roles.findIndex( id => id === payload.id );
// if the array contains the deleted role
if ( i !== -1 ) {
// remove one element starting from that position
scene.roles.splice( i, 1 )
}
})
}
}
I am trying to make a todo app using redux and I'm stack on how to delete a todo from the array.
reducer.js
export default function todo(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
case 'REMOVE_TODO':
return {
id: action.id,
...state.slice(id, 1)
}
default:
return state;
}
}
action.js
let nextTodoId = 0
export const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
})
export const removeTodo = id => {
type: 'REMOVE_TODO',
id
}
So far i can add and toggle a todo as completed or not. Thanks
Using redux you need to return all array elements except the removed one from reducer.
Personally, I prefer using the filter method of the Array. It'll return you a shallow copy of state array that matches particular condition.
case 'REMOVE_TODO':
return state.filter(({id}) => id !== action.id);
In react redux application, you should know, you always have to create a new object,
to deleting an item please use spread operator like this :
return [...state.filter(a=>a.id !== id)]
I am making my first app using React and Redux. My state will need to store some lists that will have an author and a title and on each list, I will be storing 10 items(URL, description etc). I am struggling to find a good way to keep my state organized so that is easy to manage and scale. After some research, I decided to use objects with ID's instead of an array. My actions look like this:
const addList = (
{
id,
listAuthor = '',
listTitle = '',
} = {}
) => {
return {
type: 'ADD_LIST',
id: uuid(),
list: {
listAuthor,
listTitle,
}
}
};
const addTrack = (
{
id,
url = '',
trackInfo = '',
description = '',
} = {}
) => ({
type: 'ADD_TRACK',
track: {
id: uuid(),
url,
trackInfo,
description
}
});
My ADD_LIST reducer looks like this
export default (state = listReducerDefaultState, action) => {
switch (action.type) {
case 'ADD_LIST':
return {
listIdArray: [...state.listIdArray, action.id],
listById: {
...state.listById,
[action.id]: action.list,
}
};
What would be the best way to write my ADD_TRACK reducer so that each track has the Id of the list? And is this approach right or should I be adding the tracks using the ADD_LIST action?
You need to add listId field to addTrack action, and path it as other props.
const addTrack = ({id,url = '',trackInfo = '', listId='',description = '',} = {}) => ({
type: 'ADD_TRACK',
track: {
id: uuid(),
listId,
url,
trackInfo,
description
}
});
addTrackReducer (state = {}, action) => {
switch (action.type) {
case 'ADD_TRACK':
return action.track
};
PS About your second question: Yes, this is right. You better use declarative actions as in Redux guides said. AddTrack must have own action named ADD_TRACK or any convenient for you way.
I'm build a simple app that expands and collapses sections of content based on their state. Basically, if collapse = false, add a class and if it's true, add a different class.
I'm using Next.js with Redux and running into an issue. I'd like to update the state based on an argument the action is passed. It's not updating the state and I'm not sure why or what the better alternative would be. Any clarification would be great!
// DEFAULT STATE
const defaultState = {
membership: 'none',
sectionMembership: {
id: 1,
currentName: 'Membership',
nextName: 'General',
collapse: false
},
sectionGeneral: {
id: 2,
prevName: 'Membership',
currentName: 'General',
nextName: 'Royalties',
collapse: true
}
}
// ACTION TYPES
export const actionTypes = {
SET_MEMBERSHIP: 'SET_MEMBERSHIP',
MOVE_FORWARDS: 'MOVE_FORWARDS',
MOVE_BACKWARDS: 'MOVE_BACKWARDS'
}
// ACTION
export const moveForwards = (currentSection) => dispatch => {
return dispatch({ type: actionTypes.MOVE_FORWARDS, currentSection })
}
// REDUCERS
export const reducer = (state = defaultState, action) => {
switch (action.type) {
case actionTypes.SET_MEMBERSHIP:
return Object.assign({}, state, {
membership: action.membershipType
})
case actionTypes.MOVE_FORWARDS:
const currentId = action.currentSection.id
const currentName = "section" + action.currentSection.currentName
return Object.assign({}, state, {
currentName: {
id: currentId,
collapse: true
}
})
default: return state
}
}
The currentName variable is causing an issue for the state to not update. I want to be able to dynamically change each sections state, which is why I thought I'd be able have a variable and update state like this.
It seems you can't use a variable for the key in the key/value pair. Why is this? What's an alternative to dynamically updating state?
That is because JavaScript understands that you want to create a key named currentName not a key with the value of the variable currentName. In order to do what you want, you have to wrap currentName in brackets:
return Object.assign({}, state, {
[currentName]: {
id: currentId,
collapse: true
}
})
So it will understand that the key will be whatever currentName is.
It also right:
return Object.assign({}, state, {
[currentName]: Object.assign({}, state[currentName], {
id: currentId,
collapse: true
})
})