How to properly update the reducer - reactjs

In my initial state I have boards, think of them as groups in a chat room.
setboard is a variable use to switch between rooms using
activeBoard. =>
activeBoard: state.boards[initialState.setBoard],
I map the content of debates easily. The problem comes when I try to update the reducer.
const initialState = {
setBoard: 'Feed',
boards : {
Feed: {
id: 1,
debates: [
{
id: 1,
text: 'This is the most amazing website on the internet.",
Images: 'soul.jpg',
},
{
id: 2,
topic: 'Somebody tell him to shut up',
text: "This is the most amazing website on the internet.",
Images: 'salt.jpg',
},
],
invitations: [
{
id: 1,
nickname: "imhotep",
inviteText: ' BLOCKING People block or unfriend their parents on Facebook
},
],
}
export const BoardProvider = ({ children}) =>{
const [state, dispatch] = useReducer(BoardReducer, initialState)
function AddDebates(debates){
dispatch({
type: 'Add-debates',
payload: debates
})
}
return ( <BoardContext.Provider value={{
boards: state.boards,
activeBoard: state.boards[initialState.setBoard],
debates: activeBoard.debates,
AddDebates
}}>
{children}
</BoardContext.Provider>)
}
This is my reducer.
export default ( state, action, activeBoard, debates,invitations) =>{
switch(action.type) {
case 'Add-debates':
return {
...state,
debates: [action.payload, ...debates]
}
default:
return state
}
}
I get an error: TypeError: debates is not iterable
I can render debates by simply mapping it but can update reducer this way. Some help pls...

You need to shallowly copy all levels of state from root to debates that you are updating as it is nested a few levels deep. The correct reference will also include the full path to that property, i.e. state.debates is undefined and not iterable.
case 'Add-debates':
return {
...state,
boards: {
...state.boards,
Feed: {
...state.boards.Feed,
debates: [action.payload, ...state.boards.Feed.debates],
},
},
}

Related

Trying to store a series of arrays inside a Redux-toolkit slice but only getting one

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] ?? []
})

Redux + reactflow add edges through a reducer

Hi I'm still new to redux and I'm trying to manipulate the nodes in reactflow library using redux, I've managed to add a node through a reducer but when I try to add an edge(the link between nodes) it returns a warning
can anyone helps me with how to handle it in reducer
Link to sandbox if you want to see the code
https://codesandbox.io/s/react-flow-add-node-button-with-redux-1q2dz?file=/src/store/reducer.js
warning image
This question is more a react-flow question than it is a redux question but the issue is that you aren't dispatching an action with the source and target node ids you want to connect.
App: dispatch the add edge action with the vertices/node ids you want to connect:
const AddEdge = ({ source, target }) => {
dispatch({
type: "ADD_EDGE",
source,
target,
});
};
Reducer - don't mutate the state object, just return a new state object value:
export const reducer = (state = initialElements, action) => {
switch (action.type) {
case ADD_NODE:
return [
...state,
{
id: Math.random().toString(),
position: { x: 100, y: 50 },
data: { label: "yo" }
}
];
case ADD_EDGE:
return [
...state,
{
id: Math.random().toString(),
source: action.source,
target: action.target
}
];
default:
return state;
}
};

state gets different values in react redux

the title may be misleading but here is what happened:
reducer.js:
// initial state
const initialState = {
notes: [
{
content: "reducer defines how redux store works",
important: true,
id: 1,
},
{
content: "state of store can contain any data",
important: false,
id: 2,
},
],
filter: "IMPORTANT",
};
// reducer
const noteReducer = (state = initialState, action) => {
switch (action.type) {
case "NEW_NOTE":
console.log("state", state);
return state.notes.concat(action.data);
// ...
}
const generateId = () => Math.floor(Math.random() * 1000000);
// action
export const createNote = (content) => {
return {
type: "NEW_NOTE",
data: {
content,
important: false,
id: generateId(),
},
};
};
in index.js:
const reducer = combineReducers({
notes: noteReducer,
filter: filterReducer,
});
const store = createStore(reducer, composeWithDevTools());
//dispatch a note from index.js
//it works here
store.dispatch(
createNote("combineReducers forms one reducer from many simple reducers")
);
returns in the console.log("state", state); in reducer.js:
state
{notes: Array(2), filter: 'IMPORTANT'}
filter: "IMPORTANT" // 'filter' is here
notes: (2) [{…}, {…}]
[[Prototype]]: Object //prototype is object
Here createNote is successful.
However, when creating a new note through:
const NewNote = (props) => {
const dispatch = useDispatch();
const addNote = (event) => {
event.preventDefault();
const content = event.target.note.value;
event.target.note.value = "";
// createNote does not work here
dispatch(createNote(content));
};
return (
<form onSubmit={addNote}>
<input name="note" />
<button type="submit">add</button>
</form>
);
};
Here the console.log("state", state); returns:
state
(3) [{…}, {…}, {…}]
0: {content: 'reducer defines how redux store works', important: true, id: 1}
1: {content: 'state of store can contain any data', important: false, id: 2}
2: {content: 'combineReducers forms one reducer from many simple reducers', important: false, id: 824517}
length: 3
// 'filter' is missing
[[Prototype]]: Array(0) // state prototype changed to array
In which the filter is gone from the state, so the creation is not successful.
In short, store.dispatch( createNote("...") ); works but not dispatch(createNote(content));.
The reason seems to be that noteReducer received different states. But in both cases filter is not specified.
I wonder why this happens and how to solve it?
as we know when you are using a reducer the reducer takes 2 parameters one for initial stat and the other for action.
any action will be run in the reducer you need to save the old state
by used spread operator {...state}
case "NEW_NOTE":
console.log("state", state);
{...state, notes: state.notes.concat(action.data)}
found the issue.
noteReducer should be:
const noteReducer = (state = initialState, action) => {
switch (action.type) {
case "NEW_NOTE":
return { ...state, notes: state.notes.concat(action.data) };
//...
}
just found out that above is a wrong fix. the right one is actually is:
const noteReducer = (state = initialState.notes, action) => {
otherwise noteReducer is changing the filter as well. but it should be changing the 'note' part only.

Rematch/Reducers- Error writing the test cases

I am trying to write unit test cases for my reducers.js using React Testing Library.I am getting some error which i am not able to figure out. Can someone help me understand where i am going wrong?
reducers.js-
const INITIAL_STATE = {
userData: {},
};
const setUserData = (state, { key, value }) => ({ // {key: value}
...state,
userData: {
...state.userData,
[key]: value,
},
});
reducers.test.js
import reducersDefault from './reducers';
const {
setUserData,
} = reducersDefault.reducers;
describe('reducers', () => {
it('setUserData', () => expect(setUserData({}, { key: { name: 'test' } })).toEqual({
userData: { userData: { key: { name: 'test' } } },
}));
});
With the above code, i am getting the below error-
Expected value to equal:
{"userData": {"userData": {"key": {"name": "test"}}}}
Received:
{"userData": {"undefined": undefined}}
Trying to figure out what i am doing wrong here. Any help is much appreciated.
You test fails because your function doesn't work properly. You cannot destructure an object to key/value - what you are doing currently extracts the values of key and value properties of the object you are passing there.
Here's a better approach:
const setUserData = (state, data) => ({
...state,
userData: {
...state.userData,
..data, // put every property inside data to userData
},
});
LE: After reading your comment I realised you are calling your function wrong in the test:
expect(setUserData({}, { key: 'name', value: 'test' })).toEqual({
userData: { name: 'test' }
}));
This should work as you expect (without changing setUserData).

Problem with Reducer that contains few different values

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
}

Resources