Should I check if array is null in a react selector - reactjs

Story
I am working on a react application with a state redux.
In that state, there is an array called 'students', initialized as empty. This array is updated through calls to HTTP rest services.
I have also created a selector to pick the data that I need.
student.reducer.ts :
const initialState = {
students: []
}
student.selector.ts :
export const studentsFullName = (students) => {
return students.map(
student => ({
id: student.id,
fullName:
`${student.firstName} ${student.lastName}`
})
);
};
Question
Considering that the rest service should NOT return a null object for the students, should I, in the selector, add a condition to return an empty array if 'students' is null, or is it useless ?
student.selector.ts :
export const studentsFullName = (students) => {
// is this necessary ?
if (!students){
return [];
}
return students.map(
student => ({
id: student.id,
fullName:
`${student.firstName} ${student.lastName}`
})
);
};

You can have a check like
export const studentsFullName = (students) => {
return {student.length && students.map(
student => ({
id: student.id,
fullName:
`${student.firstName} ${student.lastName}`
})
);
}
};

Thank you for your replies. Finally, I used #Zohaib solution, which is
(students || []).map(
student => ({
id: student.id,
fullName:
`${student.firstName} ${student.lastName}`
})
);

Related

Swr's cache updates but UI doesn't for no reason - swr hook Next.js ( with typescript )

I'm doing a facebook clone, and everytime i press like's button, i want to see the change immediately, that's something that swr provides, but, it only updates after 4-8 seconds :/
What i tried to do is the following: when i click like's button, i first mutate the cache that swr provides, then i make the call to the API, then revalidate data to see if everything is right with the data, actually i console log the cache and it updates immediately, but it the UI doesn't and i don't know why
Let me give sou some context with my code
This is how my publication looks like ( inside pub it's the likes property )
export type theLikes = {
identifier: string;
};
export type theComment = {
_id?: string;
body: string;
name: string;
perfil?: string;
identifier: string;
createdAt: string;
likesComments?: theLikes[];
};
export interface Ipublication {
_id?: string;
body: string;
photo: string;
creator: {
name: string;
perfil?: string;
identifier: string;
};
likes?: theLikes[];
comments?: theComment[];
createdAt: string;
}
export type thePublication = {
data: Ipublication[];
};
This is where i'm asking for all publications with getStaticProps
const PublicationsHome = ({ data: allPubs }) => {
// All pubs
const { data: Publications }: thePublication = useSWR(
`${process.env.URL}/api/publication`,
{
initialData: allPubs,
revalidateOnFocus: false
}
);
return (
<>
{Publications ? (
<>
<PublicationsHomeHero>
{/* Create pub */}
<CreatePubs />
{/* Show pub */}
{Publications.map(publication => {
return <Pubs key={publication._id} publication={publication} />;
})}
</PublicationsHomeHero>
</>
) : (
<div></div>
)}
</>
);
};
export const getStaticProps: GetStaticProps = async () => {
const { data } = await axios.get(`${process.env.URL}/api/publication`);
return {
props: data
};
};
export default PublicationsHome;
This is where the like button is ( focus on LikePub, there is where the logic is )
The conditional is simple, if user already liked a pub, cut the like, otherwise, like the pub
interface IlikesCommentsProps {
publication: Ipublication;
}
const LikesComments: React.FC<IlikesCommentsProps> = ({ publication }) => {
const LikePub = async (): Promise<void> => {
try {
if (publication.likes.find(f => f.identifier === userAuth.user.id)) {
mutate(
`${process.env.URL}/api/publication`,
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const deleteLike = currentPub.likes.findIndex(
f => f.identifier === userAuth.user.id
);
currentPub.likes.splice(deleteLike, 1);
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
},
false
);
} else {
mutate(
`${process.env.URL}/api/publication`,
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
currentPub.likes.push({ identifier: userAuth.user.id });
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
},
false
);
}
console.log(publication.likes);
await like({ identifier: userAuth.user.id }, publication._id);
mutate(`${process.env.URL}/api/publication`);
} catch (err) {
if (err) {
mutate(`${process.env.URL}/api/publication`);
}
}
};
return (
<motion.div
onClick={LikePub}
variants={Actions}
whileHover="whileHover"
whileTap="whileTap"
>
<motion.span>
<LikeIcon colorIcon="rgb(32, 120, 244)" />
</motion.span>
<LikeCommentText
separation="0.2rem"
colorText="rgb(32, 120, 244)"
>
Me gusta
</LikeCommentText>
</motion.div>
)
}
As you can see as well, i console.log publication's likes, and this is what happened
The identifier is added to the cache, meaning that user liked the pub, but the UI doesn't update, it takes 4 - 7 seconds to update, probably even more, the same thing happened with removing the like, look at this
Cut the like, but, the UI doesn't update
I'm desperate, i've tried everything, i've been trying to fix this for almost a week, but found nothing, what am i doing wrong, is this a bug ?
I believe the problem is you're directly mutating (in the javascript not swr sense) swr's data that is completely invisible to swr. And only when response is returned from the API your state is updated and that finally triggers swr's observers.
Here you may notice that currentPub.likes is an array (reference) inside currentPub object. You're directly mutating it (with splice) and then insert the same reference back into allPublications object. From swr's perspective the likes array didn't change. It still holds the same reference as before the mutation:
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const deleteLike = currentPub.likes.findIndex(
f => f.identifier === userAuth.user.id
);
currentPub.likes.splice(deleteLike, 1);
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
}
Snippet to illustrate the behavior:
const allPublications = [{ attr: 'attr', likes: [1, 2, 3] }]
const currentPub = allPublications[0]
currentPub.likes.splice(1, 1)
const updatePub = allPublications.map((pub, idx) => idx === 0 ? currentPub : pub)
console.log(updatePub[0] === allPublications[0]) // true
console.log(updatePub[0].likes === allPublications[0].likes) // true. the reference stays the same
console.log(updatePub[0]) // while the object has changed
You should rewrite it to exlude direct mutation and always return changed references for changed objects/arrays. Something like:
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const likes = currentPub.likes.filter( // filter creates new array (reference)
f => f.identifier !== userAuth.user.id
);
const updatePub = allPublications.map(pub => // map creates new array
pub._id === currentPub._id ? { ...currentPub, likes } : pub // {} creates new object (reference)
);
return updatePub;
}
const allPublications = [{ attr: 'attr', likes: [1, 2, 3] }]
const currentPub = allPublications[0]
const likes = currentPub.likes.filter((el) => el !== 2)
const updatePub = allPublications.map((pub, idx) =>
idx === 0 ? { ...currentPub, likes } : pub
)
console.log(updatePub[0] === allPublications[0]) // false
console.log(updatePub[0].likes === allPublications[0].likes) // false
console.log(updatePub[0])
And do the same for else branch mutation function.

How to get all elements from an atomFamily in recoil?

Im playing around with recoil for the first time and cant figure out how I can read all elements from an atomFamily. Let's say I have an app where a user can add meals:
export const meals = atomFamily({
key: "meals",
default: {}
});
And I can initialize a meal as follows:
const [meal, setMeal] = useRecoilState(meals("bananas"));
const bananas = setMeal({name: "bananas", price: 5});
How can I read all items which have been added to this atomFamily?
You have to track all ids of the atomFamily to get all members of the family.
Keep in mind that this is not really a list, more like a map.
Something like this should get you going.
// atomFamily
const meals = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
When creating a new objects inside the family you also have to update the mealIds atom.
I usually use a useRecoilCallback hook to sync this together
const createMeal = useRecoilCallback(({ set }) => (mealId, price) => {
set(mealIds, currVal => [...currVal, mealId]);
set(meals(mealId), {name: mealId, price});
}, []);
This way you can create a meal by calling:
createMeal("bananas", 5);
And get all ids via:
const ids = useRecoilValue(mealIds);
Instead of using useRecoilCallback you can abstract it with selectorFamily.
// atomFamily
const mealsAtom = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set}, meal) => {
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
Further more, in case you would like to support reset you can use the following code:
// atomFamily
const mealsAtom = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set, reset}, meal) => {
if(meal instanceof DefaultValue) {
// DefaultValue means reset context
reset(mealsAtom(id));
reset(mealIds (id));
return;
}
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
If you're using Typescript you can make it more elegant by using the following guard.
import { DefaultValue } from 'recoil';
export const guardRecoilDefaultValue = (
candidate: unknown
): candidate is DefaultValue => {
if (candidate instanceof DefaultValue) return true;
return false;
};
Using this guard with Typescript will look something like:
// atomFamily
const mealsAtom = atomFamily<IMeal, number>({
key: "meals",
default: {}
});
const mealIds = atom<number[]>({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily<IMeal, number>({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set, reset}, meal) => {
if (guardRecoilDefaultValue(meal)) {
// DefaultValue means reset context
reset(mealsAtom(id));
reset(mealIds (id));
return;
}
// from this line you got IMeal (not IMeal | DefaultValue)
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
You can use an atom to track the ids of each atom in the atomFamily. Then use a selectorFamily or a custom function to update the atom with the list of ids when a new atom is added or deleted from the atomFamily. Then, the atom with the list of ids can be used to extract each of the atoms by their id from the selectorFamily.
// File for managing state
//Atom Family
export const mealsAtom = atomFamily({
key: "meals",
default: {},
});
//Atom ids list
export const mealsIds = atom({
key: "mealsIds",
default: [],
});
This is how the selectorFamily looks like:
// File for managing state
export const mealsSelector = selectorFamily({
key: "mealsSelector",
get: (mealId) => ({get}) => {
return get(meals(mealId));
},
set: (mealId) => ({set, reset}, newMeal) => {
// if 'newMeal' is an instance of Default value,
// the 'set' method will delete the atom from the atomFamily.
if (newMeal instanceof DefaultValue) {
// reset method deletes the atom from atomFamily. Then update ids list.
reset(mealsAtom(mealId));
set(mealsIds, (prevValue) => prevValue.filter((id) => id !== mealId));
} else {
// creates the atom and update the ids list
set(mealsAtom(mealId), newMeal);
set(mealsIds, (prev) => [...prev, mealId]);
}
},
});
Now, how do you use all this?
Create a meal:
In this case i'm using current timestamp as the atom id with Math.random()
// Component to consume state
import {mealsSelector} from "your/path";
import {useSetRecoilState} from "recoil";
const setMeal = useSetRecoilState(mealsSelector(Math.random()));
setMeal({
name: "banana",
price: 5,
});
Delete a meal:
// Component to consume state
import {mealsSelector} from "your/path";
import {DefaultValue, useSetRecoilState} from "recoil";
const setMeal = useSetRecoilState(mealsSelector(mealId));
setMeal(new DefaultValue());
Get all atoms from atomFamily:
Loop the list of ids and render Meals components that receive the id as props and use it to get the state for each atom.
// Component to consume state, parent of Meals component
import {mealsIds} from "your/path";
import {useRecoilValue} from "recoil";
const mealIdsList = useRecoilValue(mealsIds);
//Inside the return function:
return(
{mealIdsList.slice()
.map((mealId) => (
<MealComponent
key={mealId}
id={mealId}
/>
))}
);
// Meal component to consume state
import {mealsSelector} from "your/path";
import {useRecoilValue} from "recoil";
const meal = useRecoilValue(mealsSelector(props.id));
Then, you have a list of components for Meals, each with their own state from the atomFamily.
Here is how I have it working on my current project:
(For context this is a dynamic form created from an array of field option objects. The form values are submitted via a graphql mutation so we only want the minimal set of changes made. The form is therefore built up as the user edits fields)
import { atom, atomFamily, DefaultValue, selectorFamily } from 'recoil';
type PossibleFormValue = string | null | undefined;
export const fieldStateAtom = atomFamily<PossibleFormValue, string>({
key: 'fieldState',
default: undefined,
});
export const fieldIdsAtom = atom<string[]>({
key: 'fieldIds',
default: [],
});
export const fieldStateSelector = selectorFamily<PossibleFormValue, string>({
key: 'fieldStateSelector',
get: (fieldId) => ({ get }) => get(fieldStateAtom(fieldId)),
set: (fieldId) => ({ set, get }, fieldValue) => {
set(fieldStateAtom(fieldId), fieldValue);
const fieldIds = get(fieldIdsAtom);
if (!fieldIds.includes(fieldId)) {
set(fieldIdsAtom, (prev) => [...prev, fieldId]);
}
},
});
export const formStateSelector = selectorFamily<
Record<string, PossibleFormValue>,
string[]
>({
key: 'formStateSelector',
get: (fieldIds) => ({ get }) => {
return fieldIds.reduce<Record<string, PossibleFormValue>>(
(result, fieldId) => {
const fieldValue = get(fieldStateAtom(fieldId));
return {
...result,
[fieldId]: fieldValue,
};
},
{},
);
},
set: (fieldIds) => ({ get, set, reset }, newValue) => {
if (newValue instanceof DefaultValue) {
reset(fieldIdsAtom);
const fieldIds = get(fieldIdsAtom);
fieldIds.forEach((fieldId) => reset(fieldStateAtom(fieldId)));
} else {
set(fieldIdsAtom, Object.keys(newValue));
fieldIds.forEach((fieldId) => {
set(fieldStateAtom(fieldId), newValue[fieldId]);
});
}
},
});
The atoms are selectors are used in 3 places in the app:
In the field component:
...
const localValue = useRecoilValue(fieldStateAtom(fieldId));
const setFieldValue = useSetRecoilState(fieldStateSelector(fieldId));
...
In the save-handling component (although this could be simpler in a form with an explicit submit button):
...
const fieldIds = useRecoilValue(fieldIdsAtom);
const formState = useRecoilValue(formStateSelector(fieldIds));
...
And in another component that handles form actions, including form reset:
...
const resetFormState = useResetRecoilState(formStateSelector([]));
...
const handleDiscard = React.useCallback(() => {
...
resetFormState();
...
}, [..., resetFormState]);

I get undefined from state

I have problem, in the code below I'm trying to console.log only name of the playLists from state with console.log(this.state.playLists.name); I get undefined.
getPlaylist() {
spotifyApi.getUserPlaylists().then((response) => {
if (response.items) {
const items = response.items;
console.log(items);
const playListsId = items.map((obj) => {
const playList = {
name: obj.name,
id: obj.id,
};
return playList;
});
console.log(playListsId);
this.setState({
playLists: playListsId,
});
}
console.log(this.state.playLists.name);
});
}
Since this.state.playLists is an array you cannot get name on it. you have to get the name of all elements in an array.
Try this in place of console.log(this.state.playLists.name);
this.state.platLists.map(playList => console.log(playList.name));

Iterating ngrx8 reducers

Interface:
export interface IClient extends Array<IClient> {
client_name: string
}
Actions:
export const addClientSuccess = createAction(
'[CLIENT] ADD_CLIENT_COMPLETE',
props<{ client_name: IClient }>()
);
Reducers:
export interface ClientState {
client_name: IClient[]
}
export const clientInitialState: ClientState = {
client_name: []
}
export const clientReducer = createReducer(
clientInitialState,
on(ClientActionTypes.addClientSuccess,(state, {client_name}) => ({
...state,
client_name: [...client_name]
})
))
Effects:
addClient = createEffect(() => {
return this.actions.pipe(
ofType(ClientActionTypes.addClient),
switchMap(({ client }) => {
return this.clientService.addClient(client).pipe(
map((res) => ClientActionTypes.addClientSuccess(res)),
catchError(error => {
return of(ClientActionTypes.addClientFailure({ error }))
})
);
})
);
});
With the above code I'm trying to append arrays to client_name but the value I enter gets separated into multiple values inside an array, I have attached the redux store on how it gets iterated.
For example if I have test and testing entered it should print a ['test',"testing"]
youre doing it wrong.
your interface should not be extending Array<IClient>. its a simple string.
export interface IClient {
client_name: string;
}
your state should contain meaningful names: client_name**s**: IClient[]
export interface ClientState {
client_names: IClient[]
}
export const clientInitialState: ClientState = {
client_names: []
}
your reducer should be
a. configured correctly, to have type safety.
b. your client_name array, should contain previous clients initialized before adding a new client. use the spread operator.
export const clientReducer = createReducer(
clientInitialState,
on(ClientActionTypes.addClientSuccess,(state, action) => ({
...state,
client_names: [...state.client_names, action.client_name]
})
your effect should also be configured correctly:
addClient$ = createEffect(() => {
return this.actions.pipe(
ofType(ClientActionTypes.addClient),
switchMap((action) => {
return this.clientService.addClient(action.client_name).pipe(
map((res) => ClientActionTypes.addClientSuccess({ client_name: res})),
catchError(error => {
return of(ClientActionTypes.addClientFailure({ error }))
})
);
})
);
});
I haven't tested this code, so use your intellisense to fix mistypes if any.
sorry on the code instead of code-blocks formatting, SO 4 spaces isn't working properly for some reason.

React/Redux updating a certain value in an array of objects

I am just learning redux and this is my first time using it in a project. I am trying to update a certain value in an array of objects. The structure of my object is:
students: {
loading: false,
error: null,
data: [{
id: 1,
name: "Bob",
email: 'whatever#gmail.com',
status: 'out'
}]
}
Below are my actions for this and the data it gets back is the id of the student that it needs to update. These work fine.
export const studentCheckInStart = student => ({
type: "STUDENT_CHECK_IN_START",
student
})
export const studentCheckIn = (id) => {
return dispatch => {
dispatch(studentCheckInStart())
return axios.put('http://localhost:8080/studentList/'+id)
.then((response) => {
dispatch(studentCheckInSuccess(response.data))
}).catch(err => {
dispatch(studentCheckInError(err))
})
}
}
export const studentCheckInSuccess = (data) => {
return {
type: STUDENT_CHECK_IN_SUCCESS,
payload: data
}
}
export const studentCheckInError = (error) => {
return {
type: STUDENT_CHECK_IN_ERROR,
error
}
}
Where I'm having the issue is in the reducer
case "STUDENT_CHECK_IN_SUCCESS":
let updatedStudent = state.students.data.findIndex((student) => {
return student.id === action.payload
})
console.log(updatedStudent)
return {
...state,
students: {
...state.students[updatedStudent],
data: {
status:'in'
}
}
};
break;
case "STUDENT_CHECK_IN_START":
return {
...state,
students: {
...state.students,
loading: true
}
}
break;
case "STUDENT_CHECK_IN_ERROR":
return {
...state,
students: {
...state.students,
error: action.payload,
loading: false
}
}
break;
I'm trying to target the specific student object using the id to find the index of the student I want to target. Then change just the status of that object to "in". I know what I have in the STUDENT_CHECK_IN_SUCCESS is incorrect, I'm just not sure how to do it.
Your state seems a little bit complex. Why do you need loading or error in your students object? What other parts do you have in your state beside students? This is one possible way I can think of at this situation (just the related part) :
let updatedStudent = state.students.data.findIndex(
student => student.id === action.payload
);
const newData = [ ...state.students.data ];
newData[ updatedStudent ] = { ...newData[ updatedStudent ], status: "in" }
return { ...state, students: { ...state.students, data: newData } };
I will edit my answer if I think a better way.
It looks like your action doesn't really need all that payload, just an id of the student who checked in. So if you change that, I think you could return this from your reducer action:
return {
...state,
students: {
...state.students,
data: state.students.data.map(s => {
if (s.id === action.id) {
return { ...s, status: 'in' };
}
return s;
}
}
};
The idea is that you need to return everything unchanged except the data array. By using map, we can return a modified version of the data array where the student whose id matches the one supplied in the action will have their status changed to in, but the rest of the students in the data array remain unchanged.

Resources