I have a function that is using react-table as a datagrid. It is being initially populated from Apollo in a parent component via local state with each line in the grid an object in an array.
When changes occur in a cell in the grid the whole line object is written to state.
I am trying to use useEffect to trigger a mutation that writes these changes in state back to the database, but I am struggling with two main things:
the mutation is not writing back to the database (the mutation does work in the graphql playground though)
understanding how to send only the changed row back to the mutation.
The Main Function (part of)
function Table2({ columns, data }) {
const [lines, setLines] = useState(data);
const [updateLine, {loading, error }] = useMutation(UPDATE_ITEM_MUTATION, {
variables:{ ...lines}
});
useEffect(() => {
updateLine
},[lines]);
const updateMyData = (rowIndex, columnID, value) => {
setLines(getLines =>
getLines.map((row, index) => {
if (index === rowIndex) {
console.log(row)
return {
...lines[rowIndex],
[columnID]: value
};
}
return row;
})
);
};
and the mutation...
const UPDATE_ITEM_MUTATION = gql`
mutation UPDATE_LINE_MUTATION(
$id: ID!,
$description: String,
$project:Int
$category:Int
$account:Int
$amt:Int
$units:String
$multiple:Int
$rate:Int
){
updateLine(
where:{id: $id},
data: {
description: $description
project: $project
category: $category
account: $account
amt: $amt
units: $units
multiple: $multiple
rate: $rate
}) {
id
description
amt
}
}
`
I'd be really grateful for some advice.
Thanks
I don't think you need to use useEffect, you can trigger the mutation in your update:
function Table2 ({ columns, data }) {
const [lines, setLines] = useState(data)
const [updateLine, { loading, error }] = useMutation(UPDATE_ITEM_MUTATION)
const updateMyData = (rowIndex, columnID, value) => {
const updatedLine = { ...lines[rowIndex], [columnID]: value }
updateLine({ variables: { ...updatedLine } })
setLines(getLines => getLines.map((row, index) => (index === rowIndex ? updatedLine : row)))
}
}
If you did want to use useEffect, you could e.g. keep the last changed line in a state variable and then use that to trigger the update:
function Table2 ({ columns, data }) {
const [lines, setLines] = useState(data)
const [updateLine, { loading, error }] = useMutation(UPDATE_ITEM_MUTATION)
const [updatedLine, setUpdatedLine] = useEffect(null);
useEffect(()=>{
// call your mutation
}, [updatedLine]);
const updateMyData = (rowIndex, columnID, value) => {
const updatedLine = { ...lines[rowIndex], [columnID]: value }
setUpdatedLine(updatedLine);
updateLine({ variables: { ...updatedLine } })
setLines(getLines => getLines.map((row, index) => (index === rowIndex ? updatedLine : row)))
}
}
Related
So I'm making a kanban board style task manager using react and react query. My current implementation of the data fetching is like the following:
const { data } = useQuery('listCollection', getListCollection)
and the content of data is something like this:
// data
{
listOrder: number[]
lists: IList[]
}
interface IList {
id: number
title: string
todoOrder: number[]
todos: ITodo[]
}
interface ITodo {
id: number
text: string
checked: boolean
}
So basically a list collection contains multiple lists and each lists contain multiple todos.
Now, I want this application to do optimistic update on each mutation (add a new todo, delete, check a todo, etc).
Here is my current implementation of optimistic update when toggling a todo check:
const mutation = useMutation(
(data: { todoId: number; checked: boolean }) =>
editTodo(data),
{
onMutate: async (data) => {
await queryClient.cancelQueries('listCollection')
const previousState = queryClient.getQueryData('listCollection')
queryClient.setQueryData('listCollection', (prev: any) => ({
...prev,
lists: prev.lists.map((list: IList) =>
list.todoOrder.includes(data.todoId)
? {
...list,
todos: list.todos.map((todo) =>
todo.id === data.todoId
? { ...todo, checked: data.checked }
: todo,
),
}
: list,
),
}))
return { previousState }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(parent, context?.previousState)
},
onSuccess: () => queryClient.invalidateQueries(parent),
},
)
As you can see, that's overly complicated. How should I approach this?
The best way to update the deeply nested data in react query is by using "Immer" library. It is very light weight and it uses proxies to change the reference of only updated data, and reducing the cost of rendering for non-updated data.
import produce from "immer";
const mutation = useMutation(
(data: { todoId: number; checked: boolean }) =>
editTodo(data),
{
onMutate: async (data) => {
await queryClient.cancelQueries('listCollection')
const previousState = queryClient.getQueryData('listCollection')
const updData = produce(previousState, (draftData) => {
// Destructing the draftstate of data.
let {lists} = draftData;
lists = lists.map((list: IList) => {
// Mapping through lists and checking if id present in todoOrder and todo.
if(list.todoOrder.includes(data.todoId) && list.todo[data.id]){
list.todo[data.id].checked = data.checked;
}
return list.
}
// Updating the draftstate with the modified values
draftData.lists = lists;
}
// Setting query data.
queryClient.setQueryData("listCollection", updData);
return { previousState }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(parent, context?.previousState)
},
onSuccess: () => queryClient.invalidateQueries(parent),
},
)
This will solve your case. You can modify the listOrder if needed just the way I updates lists.
Considering this state, I need to select some data from it:
const initialState: PlacesStateT = {
activeTicket: null,
routes: {
departure: {
carriageType: 'idle',
extras: {
wifi_price: 0,
linens_price: 0,
},
},
arrival: {
carriageType: 'idle',
extras: {
wifi_price: 0,
linens_price: 0,
},
},
},
};
so, I came up with two approaches:
first:
const useCoaches = (dir: string) => {
const name = mapDirToRoot(dir);
const carType = useAppSelector((state) => state.places.routes[name].carriageType);
const infoT = useAppSelector((state) => {
return state.places.activeTicket.trainsInfo.find((info) => {
return info.routeName === name;
});
});
const coaches = infoT.trainInfo.seatsTrainInfo.filter((coach) => {
return coach.coach.class_type === carType;
});
return coaches;
};
and second:
const handlerActiveCoaches = (name: string) => (state: RootState) => {
const { carriageType } = state.places.routes[name];
const { activeTicket } = state.places;
const trainInfo = activeTicket.trainsInfo.find((info) => {
return info.routeName === name;
});
return trainInfo.trainInfo.seatsTrainInfo.filter((coach) => {
return coach.coach.class_type === carriageType;
});
};
const useActiveInfo = (dir: string) => {
const routeName = mapDirToRoot(dir);
const selectActiveCoaches = handlerActiveCoaches(routeName);
const coaches = useAppSelector(selectActiveCoaches);
return coaches;
};
Eventually, if the first one works ok then the second one gives a lot of useless re-renders in component. I suspect that there are problems with selectActiveCoaches closure, maybe react considers that this selector is different on every re-render but I am wrong maybe. Could you explain how does it work?
selectActiveCoaches finishes with return seatsTrainInfo.filter(). This always returns a new array reference, and useSelector will force your component to re-render whenever your selector returns a different reference than last time. So, you are forcing your component to re-render after every dispatched action:
https://react-redux.js.org/api/hooks#equality-comparisons-and-updates
One option here would be to rewrite this as a memoized selector with Reselect:
https://redux.js.org/usage/deriving-data-selectors
I have a problem trying to update an Array of Objects that lives in a Themecontext, my problem is with mutation, I'm using Update from Immutability helpers. the thing is that when I update my array in my specific element, This appears at the end of my object.
This is my code:
function changeValueOfReference(id, ref, newValue) {
const namevalue = ref === 'colors.primary' ? newValue : '#';
console.warn(id);
const data = editor;
const commentIndex = data.findIndex(function(c) {
return c.id === id;
});
const updatedComment = update(data[commentIndex], {styles: { value: {$set: namevalue} } })
var newData = update(data, {
$splice: [[commentIndex, 1, updatedComment]]
});
setEditor(newData);
this is my result:
NOTE: before I tried to implement the following code, but this mutates the final array and break down my test:
setEditor( prevState => (
prevState.map( propStyle => propStyle.styles.map( eachItem => eachItem.ref === ref ? {...eachItem, value: namevalue}: eachItem ))
))
Well, I finally understood the issue:
1 - commentIndex always referenced to 0
The solution that worked fine for me:
1 - Find the index for the Parent
2 - Find the index for the child
3 - Add an array []
styles : { value: {$set: namevalue} } => styles :[ { value: [{$set: namevalue}] } ]
Any other approach is Wellcome
Complete Code :
function changeValueOfReference(id, referenceName, newValue) {
const data = [...editor];
const elemIndex = data.findIndex((res) => res.id === id);
const indexItems = data
.filter((res) => res.id === id)
.map((re) => re.styles.findIndex((fil) => fil.ref === referenceName));
const updateItem = update(data[elemIndex], {
styles: {
[indexItems]: {
value: { $set: namevalue },
variableref: { $set: [''] },
},
},
});
const newData = update(data, {
$splice: [[elemIndex, 1, updateItem]],
});
setEditor(newData);
}
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 have created a hook to access the collections of the database and its methods.
import { remote } from 'electron'
import { useState, useEffect } from "react"
function useCollections(collections = []) {
let [dbInstances, setDbInstances] = useState(null)
let [data, setData] = useState(null)
// DB METHODS
// Create
let create = async (doc, dbName) => {
await dbInstances[dbName].create(doc)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// Get details
let getDetails = async (id, dbName) => {
let doc = await dbInstances[dbName].read(id)
return doc
}
// Delete
let deleteOne = async (id, dbName) => {
await dbInstances[dbName].deleteOne(id)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// Update
let updateOne = async (id, updatedDoc, dbName) => {
await dbInstances[dbName].archive(id, updatedDoc)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// EFFECTS
useEffect(() => {
console.log('mounting component')
let newDBIs = {}
collections.forEach(col => newDBIs[col] = remote.getGlobal(col))
console.log('db instances settted', newDBIs)
setDbInstances(newDBIs)
}, [])
// When DBs are instantiated, request all docs and set data with response
useEffect(() => {
if (
dbInstances !== null &&
data === null &&
Object.keys(dbInstances).length === collections.length)
{
console.log('setting data')
let newData = {}
collections.forEach(async col => newData[col] = await dbInstances[col].readAll())
console.log('data setted => ', newData)
setData(newData)
}
}, [dbInstances])
return {
data,
create,
getDetails,
deleteOne,
updateOne
};
}
export default useCollections;
In the component where the data returned by the hook is consumed, even though the variable data contains the expected data, these are not rendered.
import WindowsLayout from "../../components/layout/WindowsLayout"
import { useState, useEffect } from "react"
import { remote } from "electron"
import useCollections from "../../hooks/useCollections"
const EditWorkWindow = ({ workId }) => {
let { data, deleteOne, updateOne } = useCollections([
'workDB',
'studioDB',
'rateDB'
])
useEffect(() => {
if (data !== null) console.log(data)
}, [data])
return (
<WindowsLayout title="Edit work window">
<div style={{ height: 243 }} className="window-content">
<div className="padded-more bg-gray-200">
<h2>{JSON.stringify(data)}</h2>
<button onClick={() => console.log(data)}>CLG</button>
</div>
</div>
</WindowsLayout >
)
}
export default EditWorkWindow
The effect hook shows the expected data by console.
<h2>{JSON.stringify(data)}</h2> = {}
When the button is clicked, the expected data is displayed on the console.
I can't understand why if data contains properties, they are not shown in {JSON.stringify(data)}
This is what is shown by the console after clicking on the button
console.log(data) image
And this is example data and their its properties
{
"workDB":[
{
"product":"Work name 1",
"amounts_rates":[
{
"rate":"EflcQflqu2oWWVk2",
"amount":6
},
{
"rate":"FeMIX00pwpmZwoVW",
"amount":1
}
],
"date":"2020-08-31",
"studio":"BCvPeWzMiS8fZsmS",
"_id":"2ZvHMWFODBHYWEBo",
"createdAt":"2020-08-31T09:39:21.077Z",
"updatedAt":"2020-08-31T09:39:21.077Z"
},
{
"product":"Work name 2",
"amounts_rates":[
],
"date":"2020-09-02",
"director":"",
"_id":"PRpp1OQcJnkFKeR0",
"createdAt":"2020-09-01T19:56:33.201Z",
"updatedAt":"2020-09-01T19:56:33.201Z"
}
],
"studioDB":[
{
"name":"Studio name 1",
"_id":"0J1AVXtgDjwBjRS9",
"createdAt":"2020-08-25T10:18:40.004Z",
"updatedAt":"2020-08-25T10:18:40.004Z"
},
{
"name":"Studio name 2",
"_id":"8sFH7gncaM6V7lHh",
"createdAt":"2020-08-25T10:19:45.232Z",
"updatedAt":"2020-08-25T10:19:45.232Z"
}
],
"rateDB":[
{
"name":"Rate name 1",
"value":4.1,
"_id":"EflcQflqu2oWWVk2",
"createdAt":"2020-08-25T10:24:17.357Z",
"updatedAt":"2020-08-25T10:24:17.357Z"
},
{
"name":"Rate name 1",
"value":34,
"_id":"FeMIX00pwpmZwoVW",
"createdAt":"2020-08-25T10:24:25.628Z",
"updatedAt":"2020-08-25T10:24:25.628Z"
}
]
}
Async problem it is.
// When DBs are instantiated, request all docs and set data with response
useEffect(() => {
if (
dbInstances !== null &&
data === null &&
Object.keys(dbInstances).length === collections.length)
{
console.log('setting data')
let newData = {}
collections.forEach(async col => newData[col] = await dbInstances[col].readAll())
console.log('data setted => ', newData)
setData(newData) // <-- 👋LOOK HERE
}
}, [dbInstances])
So you let newData = {} empty object, and send it off to trigger an update re-render by calling setData(), but newData is empty at the moment of calling.
In your rendering function JSON.stringify(data) pickup the data, but it's still empty at the moment of rendering!
It's only when async col => newData[col] = await someValue call is resolved, that your newData's properties will get assigned new values, the newData object stays the same. But by the time it's resolved, the rendering is done already.
Solution: wait till async function call is resolved, then you call setData()
useEffect(() => {
// ...
const promises = collections.map(async col => {
newData[col] = await dbInstances[col].readAll())
})
Promise.all(promises).then(() => { setData(newData) })
})
The reason why you see the updated value when inspecting in console, is because you didn't inspect "quick enough". By the time you mouse-click to expand the object in console, its properties are already assigned values. But if you change
console.log('data setted => ', newData)
// to
console.log('data setted => ', JSON.stringify(newData))
you'll see an empty object.