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.
Related
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'm kind of new to React.js & Redux, so I have encountered a problem with Reducers.
I am creating a site that have a main "Articles" page, "Question & Answers" page, I created for each one a separate Reducer that both work just fine.
The problem is in "Main Page" which contains a lot of small different pieces of information, and I don't want to create each little different piece of information its on Reducer, so I am trying to create one Reducer which will handle a lot of very small different pieces of information, and I can't make that work, inside the main "Content" object, I put 2 Key Value Pairs that each have an array, one for each different information, one is "Features" info, and one for the "Header" info.
This is the error that I'm getting:
Uncaught TypeError: Cannot read property 'headerContent' of undefined
at push../src/reducers/ContentReducer.js.__webpack_exports__.default (ContentReducer.js:15)
I am not sure what's the problem, maybe my code is wrong or maybe my use of the spread operator, any solution?
I have added the necessary pages from my code:
ACTIONS FILE
export const addFeatureAction = (
{
title = 'Default feature title',
feature = 'Default feature',
} = {}) => ({
type: 'ADD_FEATURE',
features: {
id: uuid(),
title,
feature
}
})
export const addHeaderAction = (
{
title = 'Default header title',
head = 'Default header',
} = {}) => ({
type: 'ADD_HEADER',
header: {
id: uuid(),
title,
head
}
})
REDUCER FILE:
const defaultContentReducer = {
content: {
featuresContent: [],
headerContent: [],
}
}
export default (state = defaultContentReducer, action) => {
switch(action.type) {
case 'ADD_FEATURE':
return [
...state.content.featuresContent,
action.features
]
case 'ADD_HEADER':
return [
...state.content.headerContent,
action.header
]
default:
return state
}
}
STORE FILE:
export default () => {
const store = createStore(
combineReducers({
articles: ArticleReducer,
qnaList: QnaReducer,
content: ContentReducer
})
);
return store;
}
The reducer function is supposed to return the next state of your application, but you are doing a few things wrong here, you are returning an array, a piece of the state and not the state object, I would suggest you look into immer to prevent this sort of errors.
Simple fix:
export default (state = defaultContentReducer, action) => {
switch(action.type) {
case 'ADD_FEATURE':
return {...state, content: {...state.content. featuresContent: [...action.features, ...state.content.featuresContent]}}
// More actions are handled here
default:
return state
}
}
If you use immer, you should have something like this
export default (state = defaultContentReducer, action) => {
const nextState = produce(state, draftState => {
switch(action.type) {
case 'ADD_FEATURE':
draftState.content.featuresContent = [...draftState.content.featuresContent, ...action.features]
});
break;
default:
break;
return nextState
}
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;
}
});
I have an app which get all categories and products from the server with Redux ACTIONS. I need to filter products with a category Id. after load data action is complete, i call another action to filter products but i'm a little bit confused.
There is codes of few parts of the app:
ProductsActions:
export const GET_INITIAL_PRODUCTS_DATA = "GET_INITIAL_PRODUCTS_DATA";
export const GET_INITIAL_PRODUCTS_DATA_RESULT = "GET_INITIAL_PRODUCTS_DATA_RESULT";
export const GET_INITIAL_PRODUCTS_DATA_ERROR = "GET_INITIAL_PRODUCTS_DATA_ERROR";
export const FILTER_PRODUCTS_BY_CATEGORY_ID = "FILTER_PRODUCTS_BY_CATEGORY_ID";
export const getInitialProductsData = () => ({
type: GET_INITIAL_PRODUCTS_DATA
});
export const filterProductsByCategoryId = categoryId => ({
type: FILTER_PRODUCTS_BY_CATEGORY_ID,
categoryId
});
ProductsReducers:
import {
GET_INITIAL_PRODUCTS_DATA,
GET_INITIAL_PRODUCTS_DATA_RESULT,
GET_INITIAL_PRODUCTS_DATA_ERROR,
FILTER_PRODUCTS_BY_CATEGORY_ID
} from "../actions/products";
const initialState = {
isFetching: false,
data: {},
error: null
};
const filterProductsByCategoryId = (state, action) => {
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case GET_INITIAL_PRODUCTS_DATA:
return {
...state,
isFetching: true
};
case GET_INITIAL_PRODUCTS_DATA_RESULT:
return {
...state,
isFetching: false,
data: action.result
};
case GET_INITIAL_PRODUCTS_DATA_ERROR:
return {
...state,
isFetching: false,
error: action.error
};
case FILTER_PRODUCTS_BY_CATEGORY_ID:
return {
...state,
data: filterProductsByCategoryId(state, action.categoryId)
};
default:
return state;
}
};
export default reducer;
And there is my code to call filter action:
filterProducts = (title = "A") => {
const _categories = Object.values(this.props.categories);
const selectedCategory = _categories.find(
category => category.title === title
);
this.props.dispatch(filterProductsByCategoryId(selectedCategory.id));
My questions is:
A) Is there is a way to filter my data and display them in UI and refresh them without using ACTIONS way??
B) If A's answer is No!, How can i get my state.data and filter them in FILTER_PRODUCTS_BY_CATEGORY_ID?
Thanks.
You can use the Array.prototype.filter() to return filtered result.
keep in mind that this will return an array and not a single value, which is a good thing if you are using this filter within your reducer. because your reducer's shape is an array and not an object.
Running example:
const myData = [{
name: 'some name',
id: 1
}, {
name: 'some name2',
id: 2
}, {
name: 'some name3',
id: 3
}, {
name: 'some name4',
id: 4
}]
const filterProductsByCategoryId = (state, action) => {
return state.filter(c => c.id === action.categoryId);
};
const result = filterProductsByCategoryId(myData, {categoryId: 2});
console.log(result);
I think it is more appropriate to create a selector for a singular product that will handle this kind of action, this way you will be able to return an object instead of an array with one product in it.
Not to mention the benefits of using reselect to do some memoizations.
For this task you can use the Array.prototype.find():
const myData = [{
name: 'some name',
id: 1
}, {
name: 'some name2',
id: 2
}, {
name: 'some name3',
id: 3
}, {
name: 'some name4',
id: 4
}]
const filterProductsByCategoryId = (state, id) => {
return state.find(c => c.id === id);
};
const result = filterProductsByCategoryId(myData, 2);
console.log(result);
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
})
})