Looking at the redux and ngrx, it looks like immer is the recommended library to produce a copy of the state before storing. Following the immer example, I add the following code to my reducer:
on(exampleActions.updateExample, (state, { example }) => {
return produce((state: ExampleType, draft: ExampleType) => {
draft.push({ example });
draft[1].done = true;
});
})
And typescript complains no-shadowed-variable, which conflicts with the example. Additionally, I am unable to return the value without return-type errors.
In cases where example is the a multi-level object:
const example = {
a: {
b: { c: 1 }
}
};
draft need to be fully de-referenced as well.
There isn't a lot of examples of immer and the createReducer integration as this a recent change for 2019. Should I disable the no-shadowed-variable rule for immer or is there a better pattern to confirm that both the state and example are properly de-referenced. example is an object of multiple levels.
Alternatively, I can avoid using immer and use ramda clone or attempt to manually deep-copy everything.
This is what ngrx-etc solves, with the mutableOn function (uses Immer)
const entityReducer = createReducer<{ entities: Record<number, { id: number; name: string }> }>(
{
entities: {},
},
mutableOn(create, (state, { type, ...entity }) => {
state.entities[entity.id] = entity
}),
mutableOn(update, (state, { id, newName }) => {
const entity = state.entities[id]
if (entity) {
entity.name = newName
}
}),
mutableOn(remove, (state, { id }) => {
delete state.entities[id]
}),
)
The source code can be found here, which should you take to the right direction.
Related
I have a pimRegistration state initialization as shown in the chrome redux-devtools screen capture below. The nesting being referenced is pimRegistration (state.domain.patient):
I updated the patient.name object with the following spread operator statement:
store.update((state) => ({
...state,
...patientPath,
...{ [property]: value },
}));
...where property is the "name" property of the patient object with value. After the update, the following screenshot shows the new state:
Note that the original patient object (purple in the screenshot) is updated with the name object, duplicated and placed at the root of the state (yellow in screenshot).
I would like to overwrite the properties of the pimRegistration(state).domain.patient object, not to create a new patient object.
The state update is called as shown below.
store.update((state) => ({
...state,
...patientPath, // state.domain.patient
...{ [property]: value },
}));
I have tried my different combinations without achieving the desired result.
The complete update function is shown below.
update(property: string, path: string, value: any) {
const paths: string[] = path.split(".");
const pathReducer = (state: IRegistrationState, path_: string) => {
if (paths.length <= 0) {
return state.domain;
}
return state[path_];
};
const domainPath = state.domain;
let patientPath, nokPath, referrerPath;
if (path.includes("patient")) {
patientPath = paths.reduce(pathReducer, state);
}
if (path.includes("nok")) {
nokPath = paths.reduce(pathReducer, state);
}
if (path.includes("referrer")) {
referrerPath = paths.reduce(pathReducer, state);
}
store.update((state) => ({
...state,
...patientPath,
...{ [property]: value },
}));
}
The function above is invoked with the following statement in Angular 2.
if (this.path.includes("patient")) {
this._repo.update("name", "domain.patient", this.name);
}
Thanks
Deep updates to a store can be tricky. In your function you seem to be spreading the updates at the root rather than at the level you want the update at. This answer here outlines the usual practice to update the state. In short, something like
const newState = {
...state,
domain: {
...state.domain,
patient: {
...state.domain.patient,
[property]: value
}
}
}
Dynamically passing a path and updating this state can be… cumbersome. There are libraries that can help you do it such as immer, but you can possibly hack your way around with normal JS/TS.
I have the following state.
state = {
friends: {
nickNames: ['Polly', 'P', 'Pau'],
... here more k:v
},
}
}
And I want to update nickNames array with a value coming from an uncontrolled form through a method in my class. However, I'm having issues at the time of determine if I am setting the state properly without mutating it.
I am doing the following
updateArray = (nickName) => {
const tempDeepCopy = {
...this.state,
friends: {
...this.state.friends,
nickNames: [...this.state.friends.nickNames]
}
}
tempDeepCopy.friends.nickNames.push(nickName)
this.setState({
friends:
{
nickNames: tempDeepCopy.friends.nickNames
}
})
}
Is this the proper way of doing it? If so, is it also the most efficient given the state? I am trying to avoid helper libraries to learn how to make deep copies.
I will appreciate help since Im trying to learn immutability and it is a concept that is taking me a lot of effort.
setState() in class components does shallow merge.
So you could just ignore other "parent" keys and focus only on friends.
You can also simplify it like:
this.setState({
friends: { // only focus on friends
...this.state.friends, // do not ignore other friend k:v pairs
nicknames: [
...this.state.friends.nicknames,
nickName
]
}
})
Why not just;
updateArray = (nickName) => {
const updatedNickNames = [...this.state.friends.nickNames, nickName];
this.setState({
friends: {
...this.state.friends,
nickNames: updatedNickNames
}
});
}
Because nickNames is just a array of strings, you can copy it with the spread operator. Also, with setState you can change a specific part of your state, in your case you only have to worry about the friends part.
You can just use
this.setState({
friends:
{
nickNames: [...this.state.friends.nickNames, nickName]
}
})
State should be only modified through the setState function because if you modify it directly you could break the React component lyfecycle.
It is a correct way to do that. The thing is you must never change the state directly. That is it.
I might give a more concise code
state = {
friends: {
nickNames: ['Polly', 'P', 'Pau'],
... here more k:v
},
}
}
updateArray = (nickName) => {
this.setState(prevState => ({
...prevState,
friends:
{
...prevState.friends,
nickNames: [...prevState.friends.nickNames, nickname]
}
}))
}
As long as you do not change the state directly, any way will do through setState()
I am trying to set the breakfastMenu array in state as shown below but I can't see the array being filled in my vue-devtools.
I have properly set-up the Vuex methods and checked twice, also I didn't receive any sort of error. So, why do I have a logical error in my code?
store.js:
export default new Vuex.Store({
state: {
menu: [],
breakfastMenu: [],
lunchMenu: [],
dinnerMenu: []
},
mutations: {
'SET_MENU': (state, menuMaster) => {
state.menu = menuMaster;
},
'SET_BREAKFAST_MENU': (state, order) => {
state.breakfastMenu.unshift(order);
},
'SET_LUNCH_MENU': (state, order) => {
state.breakfastMenu.unshift(order);
},
'SET_DINNER_MENU': (state, order) => {
state.breakfastMenu.unshift(order);
},
},
actions: {
initMenu: ({ commit }, menuMaster) => {
commit('SET_MENU', menuMaster)
},
initBreakfastMenu: ({ commit, state }) => {
state.menu.forEach((element) => {
if (element.categoryId == 1) {
commit('SET_BREAKFAST_MENU', element)
}
});
},
initLunchMenu: ({ commit, state }) => {
state.menu.forEach((element) => {
if (element.categoryId == 2) {
commit('SET_LUNCH_MENU', element)
}
});
},
initDinnerMenu: ({ commit, state }) => {
state.menu.forEach((element) => {
if (element.categoryId == 3) {
commit('SET_DINNER_MENU', element)
}
});
},
},
getters: {
getBreakfastMenu(state) {
return state.breakfastMenu
},
getLunchMenu(state) {
return state.lunchMenu
},
getDinnerMenu(state) {
return state.dinnerMenu
},
}
})
Breakfast.vue:
import { mapActions, mapGetters } from 'vuex';
export default {
data() {
return {
breakfastArray: []
};
},
methods: {
...mapActions(['initBreakfastMenu']),
...mapGetters(['getBreakfastMenu']),
},
created() {
this.initBreakfastMenu;
this.breakfastArray = this.getBreakfastMenu;
}
};
No error messages so far!
I need the breakfastMenu array filled in store.js.
Please help out!
A few thoughts.
Firstly, this line:
this.initBreakfastMenu;
You aren't actually calling the method. It should be:
this.initBreakfastMenu();
Next problem is this:
...mapGetters(['getBreakfastMenu']),
The line itself is fine but it's inside your methods. It should be in the computed section.
You haven't included any sample data for state.menu but it's also worth noting that initBreakfastMenu won't do anything unless there is suitable data inside state.menu. I suggest adding some console logging to ensure that everything is working as expected there.
SET_BREAKFAST_MENU, SET_LUNCH_MENU and SET_DINNER_MENU are all modifying state.breakfastMenu. I would assume that this is incorrect and each should be modifying their respective menu.
I would also note that using local data for breakfastArray is suspicious. Generally you'd just want to use the store state directly via the computed property rather than referencing it via local data. This is not necessarily wrong, you may want to detach the component data from the store in this way, but keep in mind that both are referencing the same array so modification to one will also affect the other. You aren't taking a copy of the array, you're just creating a local reference to it.
You should also consider whether you actually need the 4 menu types within your state. If breakfastMenu, lunchMenu and dinnerMenu are all just derived from menu then you'd be better off just implementing those using getters. getters are the store equivalent of computed properties and can contain the relevant filtering logic to generate their value from state.menu.
initBreakfastMenu is an action and you may want to use this.initBreakfastMenu()
I have this json variable on my state:
this.state = {
type:
{
name: '',
type2: {
atribute: '',
parameter: [
{
value: '',
classtype: ''
}
],
name: '',
atribute1: '',
atribute2: ''
}
}
}
what I wanted to do is to add elements to my parameter list,which is empty on the beggining.
What I did was this:
addParams = () => {
let newParam = {
value: this.state.type.type2.parameter.value,
classtype: this.state.type.type2.parameter.classtype
};
/** */
this.setState(prevState => ({
type: {
// keep all the other key-value pairs of type
...prevState.type,
type2: {
...prevState.type.type2,
//this is supposed to add an element to a list
parameter: [...prevState.type.type2.parameter, newParam]
}
}
}))
}
But when executing the last line of code the following error appeared:
Uncaught TypeError: Invalid attempt to spread non-iterable instance
when spreading a list
I do not know why this does not work because the parameter is a list indeed.
If this.state.type.type2.parameter is an array then why are you referencing properties on it:
let newParam = {
value: this.state.type.type2.parameter.value,
classtype: this.state.type.type2.parameter.classtype
};
I don't think your state is structured how you expect, seems like you're replacing that array with an object at some point in your code. I suggest react-devtools to help you keep track of your state as it changes.
This isn't exactly an answer but I highly suggest using immerjs for doing these pure nested updates. I almost never recommend adding a third party library as a solution but immer is lightweight and life changing. It exports a single function called produce and uses a concept called a Proxy to perform pure updates that are written as mutations.
With immer (and your bug fixed) your code becomes this:
const newParam = {
value: this.state.type.type2.parameter.value,
classtype: this.state.type.type2.parameter.classtype
};
this.setState(produce(draftState => {
draftState.type.type2.parameter.push(newParam);
}))
It lets you write more terse code that is a lot easier to read. And yes I know that looks like a mutation, but it isn't one this is 100% pure.
Try this :
this.setState(prevState => {
return {
...prevState,
type: {
...prevState.type,
type2: {
...prevState.type.type2,
parameter: [...prevState.type.type2.parameter, newParam]
}
}
}
)
I have a mutation like
mutation deleteRecord($id: ID) {
deleteRecord(id: $id) {
id
}
}
and in another location I have a list of elements.
Is there something better I could return from the server, and how should I update the list?
More generally, what is best practice for handling deletes in apollo/graphql?
I am not sure it is good practise style but here is how I handle the deletion of an item in react-apollo with updateQueries:
import { graphql, compose } from 'react-apollo';
import gql from 'graphql-tag';
import update from 'react-addons-update';
import _ from 'underscore';
const SceneCollectionsQuery = gql `
query SceneCollections {
myScenes: selectedScenes (excludeOwner: false, first: 24) {
edges {
node {
...SceneCollectionScene
}
}
}
}`;
const DeleteSceneMutation = gql `
mutation DeleteScene($sceneId: String!) {
deleteScene(sceneId: $sceneId) {
ok
scene {
id
active
}
}
}`;
const SceneModifierWithStateAndData = compose(
...,
graphql(DeleteSceneMutation, {
props: ({ mutate }) => ({
deleteScene: (sceneId) => mutate({
variables: { sceneId },
updateQueries: {
SceneCollections: (prev, { mutationResult }) => {
const myScenesList = prev.myScenes.edges.map((item) => item.node);
const deleteIndex = _.findIndex(myScenesList, (item) => item.id === sceneId);
if (deleteIndex < 0) {
return prev;
}
return update(prev, {
myScenes: {
edges: {
$splice: [[deleteIndex, 1]]
}
}
});
}
}
})
})
})
)(SceneModifierWithState);
Here is a similar solution that works without underscore.js. It is tested with react-apollo in version 2.1.1. and creates a component for a delete-button:
import React from "react";
import { Mutation } from "react-apollo";
const GET_TODOS = gql`
{
allTodos {
id
name
}
}
`;
const DELETE_TODO = gql`
mutation deleteTodo(
$id: ID!
) {
deleteTodo(
id: $id
) {
id
}
}
`;
const DeleteTodo = ({id}) => {
return (
<Mutation
mutation={DELETE_TODO}
update={(cache, { data: { deleteTodo } }) => {
const { allTodos } = cache.readQuery({ query: GET_TODOS });
cache.writeQuery({
query: GET_TODOS,
data: { allTodos: allTodos.filter(e => e.id !== id)}
});
}}
>
{(deleteTodo, { data }) => (
<button
onClick={e => {
deleteTodo({
variables: {
id
}
});
}}
>Delete</button>
)}
</Mutation>
);
};
export default DeleteTodo;
All those answers assume query-oriented cache management.
What if I remove user with id 1 and this user is referenced in 20 queries across the entire app? Reading answers above, I'd have to assume I will have to write code to update the cache of all of them. This would be terrible in long-term maintainability of the codebase and would make any refactoring a nightmare.
The best solution in my opinion would be something like apolloClient.removeItem({__typeName: "User", id: "1"}) that would:
replace any direct reference to this object in cache to null
filter out this item in any [User] list in any query
But it doesn't exist (yet)
It might be great idea, or it could be even worse (eg. it might break pagination)
There is interesting discussion about it: https://github.com/apollographql/apollo-client/issues/899
I would be careful with those manual query updates. It looks appetizing at first, but it won't if your app will grow. At least create a solid abstraction layer at top of it eg:
next to every query you define (eg. in the same file) - define function that clens it properly eg
const MY_QUERY = gql``;
// it's local 'cleaner' - relatively easy to maintain as you can require proper cleaner updates during code review when query will change
export function removeUserFromMyQuery(apolloClient, userId) {
// clean here
}
and then, collect all those updates and call them all in final update
function handleUserDeleted(userId, client) {
removeUserFromMyQuery(userId, client)
removeUserFromSearchQuery(userId, client)
removeIdFrom20MoreQueries(userId, client)
}
For Apollo v3 this works for me:
const [deleteExpressHelp] = useDeleteExpressHelpMutation({
update: (cache, {data}) => {
cache.evict({
id: cache.identify({
__typename: 'express_help',
id: data?.delete_express_help_by_pk?.id,
}),
});
},
});
From the new docs:
Filtering dangling references out of a cached array field (like the Deity.offspring example above) is so common that Apollo Client performs this filtering automatically for array fields that don't define a read function.
Personally, I return an int which represents the number of items deleted. Then I use the updateQueries to remove the document(s) from the cache.
I have faced the same issue choosing the appropriate return type for such mutations when the rest API associated with the mutation could return http 204, 404 or 500.
Defining and arbitrary type and then return null (types are nullable by default) does not seem right because you don't know what happened, meaning if it was successful or not.
Returning a boolean solves that issue, you know if the mutation worked or not, but you lack some information in case it didn't work, like a better error message that you could show on FE, for example, if we got a 404 we can return "Not found".
Returning a custom type feels a bit forced because it is not actually a type of your schema or business logic, it just serves to fix a "communication issue" between rest and Graphql.
I ended up returning a string. I can return the resource ID/UUID or simply "ok" in case of success and return an error message in case of error.
Not sure if this is a good practice or Graphql idiomatic.