Using reselect with a date field on an object-based reducer - reactjs

I have a simple task list app. One of the screens is a "Today & Overdue" list.
The tasks reducer looks like:
{
"data": {
123: {
"id": 123,
"summary": "blah blah",
"dueDate": "2020-03-12",
"completed": true
},
456: {
"id": 456,
"summary": "some other task",
"dueDate": "2020-03-12",
"completed": false
}
},
"byId": [123, 456]
}
My list reducer looks like:
{
"data": {
919: {
"id": 919,
"name": "Today & Overdue"
},
818: {
"id": 818,
"summary": "My Cool List"
}
},
"byId": [919, 818]
}
On the "Today & Overdue" list, I need to fetch all tasks where the dueDate is today or older. I tried using reselect to optimize for the performance of the list screen, via:
# Get end of day today
const now = moment();
const endOfDay = Date.parse(now.endOf("day").utc(true).utcOffset(0).format());
const getTasksTodayOrOlder = (state) => Object.values(state.tasks.data).filter(task => Date.parse(task.dueDate) <= endOfDay);
But it appears that any time a field in the tasks data changes (i.e. completed or summary), the getTasksTodayOrOlder regenerates the selector.
Is the only way to do this to keep a cache on the tasks reducer; something like byDueDate to keep track of an array of arrays of due dates.
{
"data": ...,
"byId": ...,
"byDueDate": {
"2020-03-19": [112,123,141, ...],
"2020-03-20": [922, 939, ...],
}
}
The date cache seems like a lot of overhead and could get out of sync.
What is the recommended way to handle a reselect that will:
Will filter to tasks due today or older
Tasks that are not complete

If the output of a selector is a calculated array that uses Object.keys, Object.values or Array.prototype.filter then you can memoize it in the following way:
const { createSelector, defaultMemoize } = Reselect;
const state = [
{ val: 1 },
{ val: 2 },
{ val: 3 },
{ val: 4 },
{ val: 5 },
{ val: 6 },
{ val: 7 },
];
//pass an array to memArray like [a,b], as long as a and b are the same
// you will get the same array back even if the arrays themselves
// are not the same like when you use filter, Object.values or Object.keys
const memArray = (() => {
const mem = defaultMemoize((...args) => args);
//pass the array that this function receives to a memoized function
// as separate arguments so if [a,b] is received it'll call
// memoized(a,b)
return arr => mem(...arr);
})();//this is called an IIFE
const selectHigher = createSelector(
state => state,
(_, min) => min,
(data, min) =>
memArray(
Object.values(data).filter(({ val }) => val > min)
)
);
const one = selectHigher(state, 5);
const twoState = [...state, { val: 0 }];
const two = selectHigher(twoState, 5);
console.log('two is:',two);
console.log('one and two are equal', one === two);
const threeState = [...state, { val: 8 }];
const three = selectHigher(threeState, 5);
console.log('three is:',three);
console.log('three and two are equal', three === two);
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>

What does it mean "regenerates the selector"? Where you using this selector? If you are using it in react functional component via hook (useSelector) for example, then:
// selectors
const now = moment();
const endOfDay = Date.parse(now.endOf("day").utc(true).utcOffset(0).format());
const getTasks = (state) => state.tasks.data;
const getTasksTodayOrOlder = createSelector(getTasks, tasks =>
Object.values(tasks)
.filter(task => Date.parse(task.dueDate) <= endOfDay)
.map(({ id, dueDate }) => {id, dueDate});
// component
import { shallowEqual, useSelector } from 'react-redux';
.
.
.
const tasksTodayOrOlderList = useSelector(getTasksTodayOrOlder, shallowEqual);
Anytime something in state.tasks.data changes, getTasksTodayOrOlder will be recalculating, but you will not get re render if previous state of tasksTodayOrOlderList is shallow equally to current output of getTasksTodayOrOlder selector (all values inside objects are equal), because we passed second argument shallowEqual to our useSelector function. I used map to remove "tracking" from unnecessary properties from our data object.
And we need to split our selector into two, because we only need to recalculate if our state.tasks.data changes, not when any part of our state changes.
Also, i think your should use endOfDay as arg value to selector, because it's dynamic.

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

React State Updation Issue

My component's state is as below:
const [state, setState] = useState({
teamMembersOptions: [],
selectedTeamMember: {},
});
teamMembersOptions are being mapped from the redux state teamMembersList as below:
const teamMembersList = useSelector(state => state.get_all_team_members.team)
useEffect(() => {
if (teamMembersList)
mapTeamMembers();
}, [teamMembersList])
const mapTeamMembers = () => {
const teamMembers = [];
teamMembersList.map(member => {
const memberObject = {
'value': member.id,
'label': member.first_name.charAt(0).toUpperCase() + member.first_name.slice(1) + ' ' + member.last_name.charAt(0).toUpperCase() + member.last_name.slice(1)
}
if (member.is_leader == 1) {
memberObject.label = memberObject.label + ' (owner)'
setState({
...state,
selectedTeamMember: memberObject
})
}
teamMembers.push(memberObject)
})
setState({
...state,
teamMembersOptions: teamMembers
})
}
The state variables of selectedTeamMember and teamMemberOptions are not updating, it keeps consoling empty state. Whenever I console the local array of teamMembers inside mapTeamMembers function, it logs all the values successfully teamMembersList from Redux
also logs successfully that means teamMembersList and teamMembers are not empty. But the state is not updating. Why the setState statement inside mapTeamMembers function is not updating the state?
There are a number of things going on here and lot of them cause renders to trigger more renders which is why you are getting unexpected output.
I have add useMemo() and useCallback() around the data and calculation method respectively, and added their return values to the dependency array for useEffect(). This is to avoid the useEffect dependencies change on every render.
Calling setState() within the .map() function doesn't feel like the right choice either as each time it is called a render might occur, even though you are halfway through the mapping operation. Instead I suggest, and opted for, using .reduce() on the array and returning that result which can then be used to update the state within the useEffect hook.
Have a look at the working code below and a sample output given the defined input from teamMembersList. Note: this doesn't use Redux in the example given that it more setup to prove the concept.
import { useCallback, useEffect, useMemo, useState } from "react";
export default function App() {
const [state, setState] = useState({
teamMembersOptions: [],
selectedTeamMember: {}
});
const teamMembersList = useMemo(
() => [
{ id: 1, first_name: "John", last_name: "Smith", is_leader: 0 },
{ id: 2, first_name: "Maggie", last_name: "Simpson", is_leader: 1 }
],
[]
);
const mapTeamMembers = useCallback(
() =>
teamMembersList.reduce(
(acc, member) => {
const memberObject = {
value: member.id,
label:
member.first_name.charAt(0).toUpperCase() +
member.first_name.slice(1) +
" " +
member.last_name.charAt(0).toUpperCase() +
member.last_name.slice(1)
};
if (member.is_leader === 1) {
memberObject.label = memberObject.label + " (owner)";
acc.leader = memberObject;
}
acc.teamMembers.push(memberObject);
return acc;
},
{
teamMembers: [],
leader: ""
}
),
[teamMembersList]
);
useEffect(() => {
if (teamMembersList) {
const members = mapTeamMembers();
setState({
selectedTeamMember: members.leader,
teamMembersOptions: members.teamMembers
});
}
}, [teamMembersList, mapTeamMembers, setState]);
return (
<div>
<pre>
<code>{JSON.stringify(state, null, 4)}</code>
</pre>
</div>
);
}
The above will render out:
{
"selectedTeamMember": {
"value": 2,
"label": "Maggie Simpson (owner)"
},
"teamMembersOptions": [
{
"value": 1,
"label": "John Smith"
},
{
"value": 2,
"label": "Maggie Simpson (owner)"
}
]
}
I'd consider splitting the state object into individual state items but that's really up to you and how you want to handle the data.

updating object inside array inside object using prevState and the useState hook

I'd like to remove a nested object based on the id is equal to a passed prop. At the moment, the entire object is replaced. I'm missing something, when trying to update the state using useState probably with the way I'm looping my object?
UPDATE: The question was closed in response to available answers for updating nested objects. This question involves arrays which I believe are part of the issue at hand. Please note the difference in nature in this question with the forEach. Perhaps a return statement is required, or a different approach to the filtering on id..
my initial object looks like this:
{
"some_id1": [
{
"id": 93979,
// MORE STUFF
},
{
"id": 93978,
// MORE STUFF
}
],
"some_id2": [
{
"id": 93961,
// MORE STUFF
},
{
"id": 93960,
// MORE STUFF
}
]
}
and I go through each item as such:
for (const key in items) {
if (Object.hasOwnProperty.call(items, key)) {
const element = items[key];
element.forEach(x => {
if (x.id === singleItem.id) {
setItems(prevState => ({
...prevState,
[key]: {
...prevState[key],
[x.id]: undefined
}
}))
}
})
}
There are 3 problems in your code:
You are setting the value of key to an object while the items is expected to have an array to ids.
// current
[key]: {
...prevState[key],
[x.id]: undefined
}
// expected
[key]: prevState[key].filter(item => item.id === matchingId)
If you intend to remove an object from an array based on some condition, you should be using filter as pointed out in Owen's answer because what you are doing is something else:
const a = { xyz: 123, xyz: undefined };
console.log(a); // { xyz: undefined} - it did not remove the key
To make your code more readable, it is expected to manipulate the entire object items and then, set it to the state once using setItems - in contrast to calling setItems multiple times inside a loop and based on some condition.
This makes your code more predictable and leads to fewer re-renders.
Also, the solution to your problem:
// Define this somewhere
const INITIAL_STATE = {
"some_id1": [
{
"id": 93979
},
{
"id": 93978
}
],
"some_id2": [
{
"id": 93961
},
{
"id": 93960
}
]
};
// State initialization
const [items, setItems] = React.useState(INITIAL_STATE);
// Handler to remove the nested object with matching `id`
const removeByNestedId = (id, items) => {
const keys = Object.keys(items);
const updatedItems = keys.reduce((result, key) => {
const values = items[key];
// Since, we want to remove the object with matching id, we filter out the ones for which the id did not match. This way, the new values will not include the object with id as `id` argument passed.
result[key] = values.filter(nestedItem => nestedItem.id !== id)
return result;
}, {});
setItems(updatedItems);
}
// Usage
removeByNestedId(93961, items);
Probably a simple reduce function would work, Loop over the entries and return back an object
const data = {"some_id1": [{"id": 93979},{"id": 93978}],"some_id2": [{"id": 93961},{"id": 93960}]}
const remove = ({id, data}) => {
return Object.entries(data).reduce((prev, [nextKey, nextValue]) => {
return {...prev, [nextKey]: nextValue.filter(({id: itemId}) => id !== itemId)}
}, {})
}
console.log(remove({id: 93961, data}))
your way solution
for (const key in items) {
if (Object.hasOwnProperty.call(items, key)) {
const element = items[key];
element.forEach(x => {
if (x.id === singleItem.id) {
setItems(prevState => ({
...prevState,
//filter will remove the x item
[key]: element.filter(i => i.id !== x.id),
}))
}
})
}
}
short solution.
for(const k in items) items[k] = items[k].filter(x => x.id !== singleItemId);
const items = {
"some_id1": [
{
"id": 93979,
},
{
"id": 93978,
}
],
"some_id2": [
{
"id": 93961,
},
{
"id": 93960,
}
]
}
const singleItemId = 93979;
for (const k in items) items[k] = items[k].filter(x => x.id !== singleItemId);
console.log(items);
//setItems(items)
You could try using the functional update.
const [data, setData] = [{id:1},{id:2},{id:3}...]
Once you know the id which you need to remove.
setData(d=>d.filter(item=>item.id !== id));

Using usestate hook with array when getting data from redux store

I'm getting my family from store like below from very top
const family:Family = useSelector((state:any) => state.family.family);
This is my family object
address: "No 48, Katukurunda"
enabled: true
id: 1
members: Array(2)
0: {id: "5", first_name: "Rohan", last_name: "Perera"}
1: {id: "4", first_name: "Sohani", last_name: "Perera"}
length: 2
__proto__: Array(0)
monthly_contribution: 50
name: "Mendis Family
as you can see clearly members have 2 items
So I have a UseState item to calculate row numbers like this
const [rowNumber, setRowNumber] = useState<number[]>([]);
I have a useEffect on top to setRowNumber like this
useEffect(() => {
if (family) {
family.members.forEach(() => {
setRowNumber([...rowNumber, rowNumber.length + 1]);
});
}
}, [family, setRowNumber]);
Just to check how many rows available when page loading I have added this code
useEffect(() => {
console.log(rowNumber);
}, [rowNumber]);
but above console log shows me this [1], an array with only one item.
What happened to the second member ?
I use rowNumber.map() to show available members of that Family. But when page loads it shows only one text box which is correct according to below useEffect
useEffect(() => {
console.log(rowNumber);
}, [rowNumber]);
What am I doing wrong here ?
Why rowNumber has only ONE item ?
family.members clearly has 2 items
I just realized only last value contains in rowNumber array
Could be related to the fact that setRowNumber is async. Have you tried to use a local array? Something like:
useEffect(() => {
if (family) {
let result = [];
family.members.forEach(() => {
result.push(result.length + 1);
});
setRowNumber(result);
}
}, [family, setRowNumber]);
Calling setState() in React is asynchronous, this means that setState does not update state immediately, so in the last iteration it will add 0 + 1 in your state.
try to do it like this:
useEffect(() => {
if (family) {
const rows = [];
family.members.forEach((value, i) => {
rows.push(i + 1);
});
setRowNumber(rows);
}
}, [family, setRowNumber]);
Content inside useEffect is wrong for storing array inside useState.
Use below code,
useEffect(() => {
if (family) {
family.members.map((item) => {
setRowNumber(()=> {
console.log(item); // verify result with this console log
return [...rowNumber, rowNumber.length + 1]
});
});
}
}, [family, setRowNumber]);

How to get old state of an array that is inside and object with spread operator in Redux

i'm tryin to get the old state of my productData[] with spread operator but this array is inside in object.
I have to keep old state because i need to keep my products that are actually in state and to add new products here in Redux.
i try this:
case 'ADD_TO_COMPARE':
return {
productData:[...state.productData,action.payload],
open:true,
}
but didnt work and here is only last product i put here.
Here is my code in reducer on Redux:
const initialValue = {
productData:[],
open:false
}
export const compareReducer = (state = initialValue, action) => {
switch (action.type) {
case 'ADD_TO_COMPARE':
return {
productData:[...state.productData,action.payload],
open:true,
}
case 'REMOVE_FROM_COMPARE':
return initialValue
default:
return state
}
}
My Console:
2 first products are added hard code and the when the third product is added, always override the last product that is added here in state
I don't see an obvious issue with the way you're merging the productData. I suspect either state.productData is empty on the way in or action.type isn't a match.
Syntactically what you're doing works:
// old state
const state = {
productData: [{id: 1}, {id: 2}],
otherStuff: 'bananas and wookies'
}
// action
const action = {
payload: [{id: 3}, {id: 4}]
}
const merged = { // new object literal
...state, // keep old state properties
productData: [ // overwrite the 'productData' from state with
...state.productData, // the previous state's product data
...action.payload // and the action's payload
]};
console.log(merged);
/*
{
"productData": [
{ "id": 1 }, <-- original state
{ "id": 2 },
{ "id": 3 }, <-- action payload
{ "id": 4 }
],
"otherStuff": "bananas and wookies" <-- original state
}
*/

Resources