How to modify existing data in reducer? - reactjs

I'm new to react-redux, I was working on a tutorial and I wanted to add a few features of my own.
How do I add a new method to add a new song to the existing array of song objects > I was confused because theres already a song key in combine reducers. What should I return/pass as a parameter to add a new song?
import { combineReducers } from "redux";
const songsReducer = () => {
return [
{
title: "song one",
duration: "4:30"
},
{
title: "song one",
duration: "4:00"
},
{
title: "song one",
duration: "3:28"
},
{
title: "song one",
duration: "3:50"
}
];
};
const selectedSongReducer = (selectedSong = null, action) => {
if (action.type === "SONG_SELECTED") {
return action.payload;
}
return selectedSong;
};
**const addSong = () => {};** // need help with this function
export default combineReducers({
songs: songsReducer,
selectedSong: selectedSongReducer
});

const addSong = song => ({
type: 'ADD_SONG',
payload: song,
});
const songsReducer = (songs = [], action) => {
switch (action.type) {
case 'ADD_SONG': {
return [...songs, action.payload.song]; //immutable
}
default: {
return [
{
title: 'song one',
duration: '4:30',
},
{
title: 'song one',
duration: '4:00',
},
{
title: 'song one',
duration: '3:28',
},
{
title: 'song one',
duration: '3:50',
},
];
}
}
};

The action will return a song with type ADD_SONG:
const addSong = (song) => {
return {
type: "ADD_SONG",
payload: {
song
}
}
}
In reducer:
const selectedSongReducer = (selectedSong = [], { type, payload = {} }) => {
if (type === "SONG_SELECTED") {
return payload;
}
if (type === "ADD_SONG") {
return [...selectedSong].push(payload.song) // Imutable selectedSong
}
return selectedSong;
};

Always use switch case for cleaner code
and return like this return [...selectedSong, action.payload];
to avoid mutation
const addSong = (song) => {
return {
type: "ADD_SONG",
payload: {
song
}
}
}
const selectedSongReducer = (selectedSong = [], action) => {
switch (action.type) {
case 'SONG_SELECTED': {
return [...selectedSong, action.payload.song];
}
default: null
}
};
const songsReducer = (song = [], action) => {
switch (action.type) {
case 'ADD_SONG': {
return [...songs, action.payload.song];
}
default: {
return [
{
title: 'song one',
duration: '4:30',
},
{
title: 'song one',
duration: '4:00',
},
{
title: 'song one',
duration: '3:28',
},
{
title: 'song one',
duration: '3:50',
},
];
}
}
};

Related

data from useQuery i.e. react query is returning undefined

this is my api middleware caller that i have created:
/* eslint-disable #typescript-eslint/no-var-requires */
// /* eslint-disable no-import-assign */
import Axios from 'axios';
import * as queryString from 'query-string';
const caseConverter = require('change-object-case');
import { cleanParams } from '_utilities/common';
import { REQUEST_TYPES } from './constants';
import { useLoginUser } from '_store/user';
interface IAuth {
method?: string;
url?: string;
data?: any;
params?:any,
contentType?:string,
isTransformRequestRequired?:boolean
}
export function apiCaller(
{
method = REQUEST_TYPES.GET,
url = '',
params = {},
data = {},
contentType = 'application/json',
isTransformRequestRequired = true,
}:IAuth,
) {
const user = useLoginUser.getState().user;
const token = user?.data?.token || '';
// caseConverter.options = {recursive: true, arrayRecursive: true};
return Axios({
method,
url,
params,
paramsSerializer: queryParams => queryString.stringify(caseConverter.snakeKeys(cleanParams(queryParams))),
data,
transformRequest: [reqData => isTransformRequestRequired
? JSON.stringify(caseConverter.toSnake(reqData))
: reqData],
transformResponse: [(respData) =>
{
if(typeof respData === 'string')
return JSON.parse(respData)
else {
return respData
}
}],
headers: {
Authorization: user !== null ? `Token ${ token }` : '',
'Content-Type': contentType,
},
responseType: 'json',
validateStatus: status => status >= 200 && status < 300,
})
.then(({ data: resp }) => resp)
.catch(error => {
console.log(error,'error')
// if user session is expired from server and got 401, will logout the user from application
if(error.response.status === 401) {
// store.dispatch(logoutSuccess());
} else {
throw(error);
}
});
}
this is where I am calling my apiCaller:
import { apiCaller } from '../api-caller';
import { ENDPOINTS, REQUEST_TYPES } from '../constants';
import moment from 'moment';
export const usersApi = async (params:any) => {
const res = await apiCaller(
{
method: REQUEST_TYPES.GET,
url: `${ENDPOINTS.USERS}/`,
params:{
...params,
startDatetime: params.startDatetime ? moment(params.startDatetime).utc().format('YYYY-MM-DD H:mm:ss') : '',
endDatetime: params.endDatetime ? moment(params.endDatetime).utc().format('YYYY-MM-DD H:mm:ss') : '',
},
},
);
return res;
}
this is where I am using useQuery to fetch data from backend:
import { useQuery } from '#tanstack/react-query'
import { usersApi } from '_api/users'
import { useAdminUser } from 'pages/Admin/AdminUsers/_store'
const filters = useAdminUser.getState().filters
export const useCreateProfile = () => {
const query = ['cashierShifts']
return useQuery(query, () => {
usersApi(filters)
})
}
and this is where I am using this in my component:
import React, { useState } from 'react'
import { Table } from 'antd'
import { useCreateProfile } from '_services/CashierProfile'
const columns = [
{
title: 'Day',
dataIndex: 'day',
key: 'day',
},
{
title: 'Shift',
dataIndex: 'shift',
key: 'shift',
},
{
title: 'startTime',
dataIndex: 'start',
key: 'start',
},
{
title: 'endTime',
dataIndex: 'end',
key: 'end',
},
{
title: 'Mart',
dataIndex: 'mart',
key: 'mart',
},
]
const CashierShifts = () => {
const { data, isError, isLoading, isSuccess } = useCreateProfile()
console.log(data, 'react-query')
const [result, setResult] = useState([
{
id: 54,
tile: 'slots 1',
date: '22-10-2203',
startTime: '8:00',
mart: {
id: 21,
martName: 'Johar Town',
},
endTime: '12:00',
cashier: {
name: 'Jamal',
id: 54,
},
},
{
id: 54,
tile: 'slots 1',
date: '22-10-2203',
startTime: '8:00',
mart: {
id: 21,
martName: 'Johar Town',
},
endTime: '12:00',
cashier: {
name: 'Jamal',
id: 54,
},
},
{
id: 54,
tile: 'slots 1',
date: '22-10-2203',
startTime: '8:00',
mart: {
id: 21,
martName: 'Johar Town',
},
endTime: '12:00',
cashier: {
name: 'Jamal',
id: 54,
},
},
{
id: 54,
tile: 'slots 1',
date: '22-10-2203',
startTime: '8:00',
mart: {
id: 21,
martName: 'Johar Town',
},
endTime: '12:00',
cashier: {
name: 'Jamal',
id: 54,
},
},
])
const dataSource = result.map((a) => ({
day: a.date,
shift: a.tile,
start: a.startTime,
end: a.endTime,
mart: a.mart.martName,
}))
return (
<div>
<Table columns={columns} dataSource={dataSource} />
</div>
)
}
export default CashierShifts
the problem is when i am using my react query hook in my component but the data is returning undefined here.
const { data, isError, isLoading, isSuccess } = useCreateProfile()
console.log(data, 'react-query')
return useQuery(query, () => {
return usersApi(filters)
})

React Redux form with connect()?

i am trying to build a react redux form using the connect() instead useSelector and useDispatch.
I managed to display the list of data and to reset the forms. But i didn't manage to send data. Here is the code:
Reducer.js
const initialState = {
tasks: {
name: "",
age: "",
job: "",
},
list: [
{
id: 0,
name: "Maillard",
age: 35,
job: "soldier",
},
],
};
export const toDoReducer = (state = initialState, action) => {
switch (action.type) {
case "name":
return {
...state,
tasks: {
name: action.payload,
},
};
case "age":
return {
...state,
tasks: {
age: action.payload,
},
};
case "job":
return {
...state,
tasks: {
job: action.payload,
},
};
case "clear":
return {
...state,
tasks: {
name: "",
age: "",
job: "",
},
};
case "add":
return {
...state,
tasks: {
...state.tasks,
id: state.list.length + 1,
},
};
default:
return {
...state,
};
}
};
export default toDoReducer;
import React from "react";
import { connect } from "react-redux";
import {
setName,
setAge,
setJob,
clearForm,
addForm,
} from "../../redux/action";
export const Form = (props) => {
return (
<div>
Name
<input value={props.list.name} onChange={(e) => { props.Name(e.target.value) }} />
Age
<input value={props.list.age} onChange={(e) => { props.Age(e.target.value) }} />
Profession
<input value={props.list.job} onChange={(e) => { props.Job(e.target.value) }} />
<div style={{ padding: 20 }}>
<button onClick={props.Clear}>Reset</button>
<button onClick={props.Add}>Envoyer</button>
</div>
</div>
);
};
const mapDispatchToProps = (dispatch) => {
return {
Name: () => {
dispatch({
type: "name",
setName,
});
},
Age: () => {
dispatch({ type: "age", setAge });
},
Job: () => {
dispatch({
type: "job",
setJob,
});
},
Clear: () => {
dispatch({ type: "clear", clearForm, });
},
Add: () => {
dispatch({)}
// My problem comes from the Add
<!-- begin snippet: js hide: false console: true babel: false -->
type: "add",
addForm
})
}
};
};
const mapStateToProps = (state) => ({
list: state.tasks,
});
export default connect(mapStateToProps, mapDispatchToProps)(Form);
// export default Form;
My problems comes from in mapDispatchToProps, i don't know what to do for the function Add
Your actions should be the ones defining the action type and you should call them in the mapDispatchToProps, not pass them as part of the action.
So your addForm should be something like
const addForm = () => ({
type:'add'
});
and in your mapDispatchToProps it should be like
Add: () => dispatch(addForm()),
But you have the same problem with all your dispatching
for example the Name should be
action
const setName = (payload) => ({
type:'name',
payload
});
mapDispatchToProps
Name: (nameValue) => {
dispatch(setName(nameValue));
},

redux - how to remove element in array which is nested to another array

I want to remove item by sending action "itemRemoved", to which I pass the id of the board and the id of the element to be deleted
How to edit my Reducer "case ITEM_REMOVED"?
const initialState = {
boards: [
{
id: 1,
title: "1",
items: [
{ id: 1, title: "1" },
{ id: 2, title: "2" },
],
},
{
id: 2,
title: "2",
items: [
{ id: 3, title: "3" },
{ id: 4, title: "4" },
{ id: 5, title: "5" },
],
},
],
};
const actions = {
itemRemoved: (boardId, itemId) =>
({ type: ITEM_REMOVED, boardId, itemId }),
}
const boardsReducer = (state = initialState, action) => {
switch (action.type) {
case ITEM_REMOVED:
return {
...state,
boards: [] // <-- ??????????
};
default:
return state;
}
};
I'll try something like this:
const boardsReducer = (state = initialState, action) => {
switch (action.type) {
case ITEM_REMOVED:
return {
...state,
boards: state.boards.map(b => {
if (b.id !== action.boardId) {
return b
}
const newB = {...b}
newB.items = newB.items.filter(i => i !== action.itemId)
return newB;
})
};
default:
return state;
}
};

.map is undefined when mapping through the profile.education array

I'm experiencing some difficulty with trying to map through an objects property which is an array of objects. I get back an error message that .map is undefined, basically saying there is no array. The array i want to map through is education. At times I also get a proxy error that the route is timing out and this loses the profile data i'm fetching and the profile object is empty. Is there anyway to fix this too.
My model:
import mongoose from 'mongoose'
const profileSchema = new mongoose.Schema(
{
experience: [
{
title: {
type: String,
required: true,
},
company: {
type: String,
required: true,
},
location: {
type: String,
},
from: {
type: Date,
required: true,
},
to: {
type: Date,
},
current: {
type: Boolean,
default: false,
},
description: {
type: String,
},
},
],
education: [
{
level: {
type: String,
required: true,
enum: [
'None',
'GCSE or equivalent',
'A-Level or equivalent',
'Certificate of Higher Education',
'Diploma of Higher Education',
'Bachelors',
'Masters',
'PhD',
],
},
school: {
type: String,
required: true,
},
fieldofstudy: {
type: String,
required: true,
},
city: {
type: String,
required: true,
},
from: {
type: Date,
required: true,
},
to: {
type: Date,
},
current: {
type: Boolean,
default: false,
},
},
],
skills: [
{
name: {
type: String,
required: true,
},
yearsExperience: {
type: Number,
required: true,
},
},
],
additionalInfo: {
desiredJobTitle: {
type: String,
},
desiredJobType: {
type: [String],
},
desiredSalary: {
type: Number,
},
readyToWork: {
type: Boolean,
default: false,
},
relocate: {
type: Boolean,
default: false,
},
},
savedJobs: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Job',
},
],
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
},
{ timestamps: true }
)
const Profile = mongoose.model('Profile', profileSchema)
export default Profile
My action:
import axios from 'axios'
import {
PROFILE_DETAILS_REQUEST,
PROFILE_DETAILS_SUCCESS,
PROFILE_DETAILS_FAIL,
PROFILE_CREATE_REQUEST,
PROFILE_CREATE_SUCCESS,
PROFILE_CREATE_FAIL,
PROFILE_CREATE_EDUCATION_REQUEST,
PROFILE_CREATE_EDUCATION_SUCCESS,
PROFILE_CREATE_EDUCATION_FAIL,
} from '../constants/profileConstants'
import { setAlert } from './alertActions'
export const getCurrentProfile = () => async (dispatch, getState) => {
try {
dispatch({ type: PROFILE_DETAILS_REQUEST })
const {
userLogin: { userData },
} = getState()
const config = {
headers: {
Authorization: `Bearer ${userData.token}`,
},
}
const { data } = await axios.get('/api/v1/profile/me', config)
dispatch({
type: PROFILE_DETAILS_SUCCESS,
payload: data,
})
} catch (error) {
dispatch({
type: PROFILE_DETAILS_FAIL,
payload:
error.response && error.response.data.error
? error.response.data.error
: null,
})
}
}
My reducer:
export const profileDetailsReducer = (state = { profile: {} }, action) => {
switch (action.type) {
case PROFILE_DETAILS_REQUEST:
return {
...state,
loading: true,
}
case PROFILE_DETAILS_SUCCESS:
return {
loading: false,
profile: action.payload,
}
case PROFILE_DETAILS_FAIL:
return {
loading: false,
error: action.payload,
}
case PROFILE_DETAILS_RESET:
return {
profile: {},
}
default:
return state
}
}
My dashboard component:
import React, { useEffect } from 'react'
import Moment from 'react-moment'
import { useDispatch, useSelector } from 'react-redux'
import { getCurrentProfile } from '../actions/profileActions'
import Loader from '../components/layout/Loader'
import DashboardActions from '../components/dashboard/DashboardActions'
const Dashboard = ({ history }) => {
const dispatch = useDispatch()
const profileDetails = useSelector((state) => state.profileDetails)
const { loading, error, profile } = profileDetails
const userLogin = useSelector((state) => state.userLogin)
const { userData } = userLogin
console.log(profile)
useEffect(() => {
if (!userData) {
history.push('/login')
} else {
dispatch(getCurrentProfile())
}
}, [dispatch, history, userData])
return (
<>
<h1 class='mb-4'>Dashboard</h1>
<p>Welcome {userData && userData.name}</p>
<br />
{loading ? (
<Loader />
) : (
<>
<DashboardActions />
<h2 className='my-2'>Education Details</h2>
<table className='table'>
<thead>
<tr>
<th>Level</th>
<th>Field of study</th>
<th>School</th>
</tr>
</thead>
<tbody>{profile.education.map((edu) => console.log(edu))}</tbody>
</table>
</>
)}
</>
)
}
export default Dashboard
Issue
state.profile.educatioin is undefined in your initial state.
export const profileDetailsReducer = (state = { profile: {} }, action) => { ...
Solution(s)
Define an initial state that contains an education array
const initialState = {
profile: {
education: [],
},
};
export const profileDetailsReducer = (state = initialState, action) => { ...
Or provide a fallback value from your selector
const { loading, error, profile: { education = [] } } = profileDetails;
...
<tbody>{education.map((edu) => console.log(edu))}</tbody>
Or provide the fallback in the render
<tbody>{(profile.education ?? []).map((edu) => console.log(edu))}</tbody>

can't change nested object in a reducer even when not mutating the objects and arrays

I'm trying to update a state inside a reducer, i know i shouldn't mutate the object or nested objects so I'm using map for arrays or object spread for objects. but it seems i can't really change a value that is deeply nested.
Beside the fact that i can't change the state, i really dislike how the code looks and especially the number of loops i need to do to just change one property. I feel like there is a better, readable and more performant way of doing this.
this is the state:
const items = [{
name: 'item 1',
id: 'item1',
tags: [{
id: 'tag1',
name: 'tag 1'
}, {
id: 'tag2',
name: 'tag 2'
}]
}, {
name: 'item 2',
id: 'item2',
tags: [{
id: 'tag1',
name: 'tag 1'
}, {
id: 'tag4',
name: 'tag 4'
}]
}];
this is the action i'm dispatching:
const action = {
type: 'CHANGE_TAG_NAME',
payload: {
itemId: 'item2',
tagId: 'tag4',
newTagName: 'tag 44444'
}
};
this is the reducer:
const itemsReducer = (state = [], action) => {
switch (action.type) {
case 'CHANGE_TAG_NAME':
{
const itemIndex = state.findIndex(item => item.id === action.payload.itemId);
const tagIndex = state[itemIndex].tags.findIndex(t => t.id === action.payload.tagId);
const nextTag = {
...state[itemIndex].tags[tagIndex],
name: action.payload.newTagName
};
const nextTags = [
...state[itemIndex].tags.slice(0, tagIndex),
nextTag,
...state[itemIndex].tags.slice(tagIndex + 1, ),
];
const nextItem = {
...state[itemIndex],
tags: nextTags
};
const nextState = [
...state.slice(0, itemIndex),
nextItem,
...state.slice(itemIndex + 1)
];
}
default:
return state;
}
};
Your reducer should work just fine, you just forgot to return nextState in your case block.
As for less iterations i suggests this pattern:
map over the items, if the current item's id is different from the itemId you have in the payload then return it as is.
If the item's id is the same then return a new object and then map over the tags, doing the same condition like you did with the item.
If the tag's id isn't the same as the tagId in the payload return it as is, if it does the same return a new object.
Here is a running example:
const items = [{
name: 'item 1',
id: 'item1',
tags: [{
id: 'tag1',
name: 'tag 1'
}, {
id: 'tag2',
name: 'tag 2'
}]
}, {
name: 'item 2',
id: 'item2',
tags: [{
id: 'tag1',
name: 'tag 1'
}, {
id: 'tag4',
name: 'tag 4'
}]
}];
const action = {
type: 'CHANGE_TAG_NAME',
payload: {
itemId: 'item2',
tagId: 'tag4',
newTagName: 'tag 44444'
}
};
const itemsReducer = (state = [], action) => {
switch (action.type) {
case 'CHANGE_TAG_NAME':
{
const {
payload: {
itemId,
tagId,
newTagName
}
} = action;
const nextState = state.map(item => {
if (item.id !== itemId) return item;
return {
...item,
tags: item.tags.map(tag => {
if (tag.id !== tagId) return tag;
return {
...tag,
name: newTagName
}
})
}
});
return nextState;
}
default:
return state;
}
};
console.log(itemsReducer(items, action));
As for a more readable code, i suggest to use more reducers.
A thumb of rule i use is to create a reducer per entity:
itemsReducer,
itemReducer,
tagsReducer,
tagReducer.
This way each reducer will be responsible for its own data.
Here is a running example:
const items = [{
name: 'item 1',
id: 'item1',
tags: [{
id: 'tag1',
name: 'tag 1'
}, {
id: 'tag2',
name: 'tag 2'
}]
}, {
name: 'item 2',
id: 'item2',
tags: [{
id: 'tag1',
name: 'tag 1'
}, {
id: 'tag4',
name: 'tag 4'
}]
}];
const action = {
type: 'CHANGE_TAG_NAME',
payload: {
itemId: 'item2',
tagId: 'tag4',
newTagName: 'tag 44444'
}
};
const tagReducer = (state = {}, action) => {
switch (action.type) {
case 'CHANGE_TAG_NAME':
{
const {
payload: {
newTagName
}
} = action;
const nextState = {
...state,
name: newTagName
}
return nextState;
}
default:
return state;
}
}
const tagsReducer = (state = [], action) => {
switch (action.type) {
case 'CHANGE_TAG_NAME':
{
const {
payload: {
tagId
}
} = action;
const nextState = state.map(tag => {
if (tag.id !== tagId) return tag;
return tagReducer(tag, action);
});
return nextState;
}
default:
return state;
}
}
const itemReducer = (state = {}, action) => {
switch (action.type) {
case 'CHANGE_TAG_NAME':
{
const nextState = {
...state,
tags: tagsReducer(state.tags, action)
}
return nextState;
}
default:
return state;
}
}
const itemsReducer = (state = [], action) => {
switch (action.type) {
case 'CHANGE_TAG_NAME':
{
const {
payload: {
itemId
}
} = action;
const nextState = state.map(item => {
if (item.id !== itemId) return item;
return itemReducer(item, action)
});
return nextState;
}
default:
return state;
}
};
console.log(itemsReducer(items, action));
This pattern is often called reducer composition, and you don't have to include all of them in your root reducer, just use them as external pure functions that will calculate the relevant portion of the state for your other reducers.
You forgot the key word return
//.....
const nextState = [
...state.slice(0, itemIndex),
nextItem,
...state.slice(itemIndex + 1)
];
// HERE RETURN 🎈
return newState;
}
default:
return state;

Resources