Problem with Mobx (TS) using with React Functional Components - reactjs

Here is the problem.
I have simple todo store:
import { makeAutoObservable } from "mobx";
import { Todo } from "./../types";
class Todos {
todoList: Todo[] = [
{ id: 0, description: "Погулять с собакой", completed: false },
{ id: 1, description: "Полить цветы", completed: false },
{ id: 2, description: "Покормить кота", completed: false },
{ id: 3, description: "Помыть посуду", completed: true },
];
// Input: Add Task
taskInput: string = "";
// Filter: query
query: string = "";
// Filter: showOnlyCompletedTasks
showOnlyCompleted: boolean = false;
constructor() {
makeAutoObservable(this);
}
setShowOnlyCompletedState(value: boolean) {
this.showOnlyCompleted = value;
}
changeCompletionState(id: number) {
const task = this.todoList.find((todo) => todo.id === id);
if (task) task.completed = !task.completed;
}
addTask(text: string) {
if (text !== "") {
const newTodo: Todo = {
id: Number(new Date()),
description: text,
completed: false,
};
this.todoList.push(newTodo);
}
}
taskChangeInput(value: string) {
this.taskInput = value;
}
queryChangeInput(value: string) {
this.query = value;
}
}
export default new Todos();
In the app I have some tasks, which I can make completed or not-completed (by clicking on it) and also I do have some filters to filter my todo_list.
Here is the code:
of posts:
import { Todo } from "../types";
import { useMemo } from "react";
function useFilterByQuery (list: Todo[], query: string):Todo[] {
const filteredList = useMemo(()=>{
if (!query) return list
return list.filter(todo => todo.description.toLowerCase().includes(query.toLowerCase()))
}, [list, query])
return filteredList
}
export function useFilterByAllFilters (list:Todo[], query: string, showOnlyCompleted: boolean):Todo[] {
const filteredByQuery = useFilterByQuery(list, query)
const filteredList = useMemo(()=>{
if(!showOnlyCompleted) return filteredByQuery
return filteredByQuery.filter(todo => todo.completed)
}, [filteredByQuery, showOnlyCompleted])
return filteredList
}
So the description of the problem is so: when I choose show me only-Completed-tasks (setting showOnlyCompleted to true), I get expected list of tasks which are all 'completed'.
But, when I change the state of 'todo' right now, the shown list isn't changing (uncompleted task doesn't filter immediatly), it's changing only after I set showOnlyCompleted to false and back to true.
I assume it's not happening, because I don't 'update' the todoList for MobX, which I provide (using function useFilterByAllFilters) by props in My TodoList component.
In my opinion the problem is with the useMemo or with something in Mobx.
Please, help me out.

Yes, it's because of useMemo. useFilterByQuery only has [list, query] deps, but when you change some todo object to be (un)completed you don't really change the whole list, only one object inside of it. And that is why this useMemo does not rerun/recompute, so you still have the old list. Same with useFilterByAllFilters.
What you are doing is not really idiomatic MobX I would say! MobX is designed to work with mutable data and React hooks are designed to work with immutable data, so to make it work with hooks you need to do some workarounds, but it would be easier to just rewrite it like that:
class Todos {
// ... your other code
// Add computed getter property to calculate filtered list
get listByQuery() {
if (!this.query) return list
return this.todoList.filter(todo => todo.description.toLowerCase().includes(this.query.toLowerCase()))
}
// Another one for all filters
get listByAllFilters() {
if(!this.showOnlyCompleted) return this.listByQuery
return this.listByQuery.filter(todo => todo.completed)
}
}
And just use this two computed properties in your React components, that's it! No need for hooks. And these properties are cached/optimized, i.e. will only run when some of their observables change.
More info about computeds

Related

How to effectively do optimistic update for deeply nested data in react query?

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.

react state updated undefined and render nothing

I'm still learning react. The data is retrieved from the redux action and store as props. My problem is that my variable is undefined after a filter function executed. What I am trying to do is using the data from redux action, and display those variable. The state of the component turn out to be undefined and nothing display on view. Does anyone know how to fix this?
https://i.stack.imgur.com/3xyJn.png
1) Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
2) Uncaught TypeError: Cannot destructure 'this.state.currentTour' as it is undefined.
[
{
"_id": "12345",
"name": "I am first tour",
"description": "Iasofisajdaiosjdioasdmoias",
"imageUrl": "https://something1.jpg",
"dates": [
"2021-06-19T09:00:00.000Z",
"2021-07-20T09:00:00.000Z",
],
},
{
"_id": "67890",
"name": "I am second tour",
"description": "cvxbcvbcxbcvbcxb",
"imageUrl": "https://something2.jpg",
"dates": [
"2023-01-12T09:00:00.000Z",
"2023-04-22T01:00:00.000Z",
],
},
//.....rest data
]
import React, { Component } from 'react';
import './Tour.css';
import { connect } from 'react-redux';
class Tour extends Component {
constructor(props) {
super(props)
this.state = {
currentTour: {},
}
this.findSingletour = this.findSingletour.bind(this);
}
componentDidUpdate() {
const tourId = this.props.match.params._id;
let FilteredTour = this.findSingletour(tourId);
// console.log(FilteredTour); ----> undefined
if (FilteredTour !== this.state.currentTour) {
this.setState({
currentTour: FilteredTour
});
}
}
findSingletour = (tourId) => {
const notYetFilterTours = this.props.tours.tourState.data;
let filtered = [];
if (notYetFilterTours) {
filtered = notYetFilterTours.find((tour) => {
if (tour.id === tourId) return true;
return filtered; // ---> actual object get back { id: '...' , name: '...', ..... }
});
}
};
render() {
const {
name,
description,
imageUrl,
dates,
} = this.state.currentTour || {}; // ---> undefined
return (
<div>
<span>{name}</span>
<span>{description}</span>
<span>{imageUrl}</span>
<span>{dates[0]}</span>
</div>
)
}
}
const mapStateToProps = (state) => ({
tours: state.tourContainer,
});
export default connect(
mapStateToProps,
)(Tour);
Try this, I don't know does it helpful or not, but it's work for me
For warning Can't perform a React state update...
=> to not see this warning add code below, and add if(!this.mounted) return; before where ever you use this.setState
private mounted = false as boolean;
componentWillUnmount() {
this.mounted = false
}
componentWillMount() {
this.mounted = true
}
I see your function findSingletour() should return default value for it.
Example:
findSingletour = (tourId) => {
const notYetFilterTours = this.props.tours.tourState.data;
let filtered = [];
if (notYetFilterTours) {
filtered = notYetFilterTours.find((tour) => {
if (tour.id === tourId) return true;
return filtered; // ---> actual object get back { id: '...' , name: '...', ..... }
});
}
return filtered; // or something else cause I saw you had return bool or filtered
// If you do not return here result maybe is undefined
};

React setState opposite boolean in map function not changing state

I created a simple to-do list in ReactJS. It loads components for to-do items stored in a file "TodoData.js", data is stored as followed in that file:
const todosData = [
{
id:1,
text: "Take out the thrash",
completed: true
},
{
id:2,
text: "Grocery shopping",
completed: false
},
App.js uses a TodoItem.js component to render each to-do item with a map function. TodoItem.js uses conditional rendering:
if (props.item.completed == true) {
return (
<div className="todoclassDone">
<input type="checkbox"
onChange={ () => props.handleChange(props.item.id)}/>
<p className="lalatext"><del>{props.item.text}</del></p>
</div>
)
}
else { .... //same code as above but with other className.
Within App.js I use the TodoItem.js component to render each item in TodoData with a map function; if the data.completed = true background is green, else background is red.
Problem: However, the handleChange(id) function in App.js is not working properly. I loop through all objects in todosData; if the id is similar to the id of checkbox which the user clicked it should change to the opposite boolean value using todo.completed = !todo.completed However, when running this code nothing is happening. The handleChange function:
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id == id) {
todo.completed = !todo.completed;
}
return todo
})
Extra info: Above mentioned problem is especially weird because if I change the boolean value of the checkbox clicked by the user to either false or true it does work. This does not result in the desired behaviour because now I am only able to change the todo.completed once from false to true. ; In this case the handleChange function would look as follows:
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id == id) {
todo.completed = true;
}
return todo
})
Any help is highly appreciated, thanks in advance! :-)
Ciao, you could try to copy state on an array, modify array and set the state wht updated array. Something like:
handleChange(id) {
let result = this.state.todos;
result = result.map(todo => {
if (todo.id === id) todo.completed = !todo.completed;
return todo;
})
this.setState({todos: result})
}
You should return the new state in your setState callback, with the new state object.
Example todo component with relevant code:
class Todo extends Component {
state = {
todos: todosData,
}
handleChange(id) {
this.setState(prevState => {
const todos = prevState.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed
}
}
return todo;
});
return { todos };
}
}

Functional component problems React

I transformed a class component into a functional component but it looks like it does not work in a way it suppose to work and I can not find what is wrong. When I create a new object there is no name for the object and when I try to mark the object as a complete it removes all created objects at ones. I created a codesandbox here. Unfortunately, I am not too much familiar with functional component. Any help would be appreciated.
Here is my codesandbox sample:
https://codesandbox.io/s/crazy-sid-09myu?file=/src/App.js
Your Todos:
const [todos, setTodos] = useState([
{ id: uuid(), name: "Task 1", complete: true },
{ id: uuid(), name: "Task 2", complete: false }
]);
onAddHandler:
const addTodo = () =>
setTodos([...todos, { id: uuid(), name: "New Task", complete: false }]);
onSetCompleteHandler:
const setCompleteHandler = id =>
setTodos(
todos.map(todo => {
if (todo.id === id) {
return {
...todo,
complete: todo.complete ? 0 : 1
};
}
return todo;
})
);
I have created your new todos. Check out this link
Todos App
I have updated your code, please check the URL https://codesandbox.io/s/determined-morning-n8lgx?file=/src/App.js
const onComp = id => {
for (let i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
let t = { ...todos[i] };
t.complete = !t.complete;
todos[i] = t;
}
}
setTodos([...todos]); // Here todos reference need to be changed
};
And also
const onSubmit = event => {
event.preventDefault();
setTodos([
...todos,
{
id: generateNewId(),
name: newTodoName,
complete: false
}
]);
setNewTodoName("");
};
While using hooks we need to be careful about state variable updates. While manipulating arrays and objects use the spread operator to create new references which invokes child components and re-render current component.

Dealing with ag grid react and rendering a grid of checkboxes

I'm messing with ag-grid, react-apollo, and everything seems to be working fine. The goal here is to click a check box and have a mutation / network request occur modifying some data. The issue i'm having is that it redraws the entire row which can be really slow but im really just trying to update the cell itself so its quick and the user experience is better. One thought i had was to do a optimistic update and just update my cache / utilize my cache. What are some approach you guys have taken.
Both the columns and row data are grabbed via a apollo query.
Heres some code:
CheckboxRenderer
import React, { Component } from "react";
import Checkbox from "#material-ui/core/Checkbox";
import _ from "lodash";
class CheckboxItem extends Component {
constructor(props) {
super(props);
this.state = {
value: false
};
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
}
componentDidMount() {
this.setDefaultState();
}
setDefaultState() {
const { data, colDef, api } = this.props;
const { externalData } = api;
if (externalData && externalData.length > 0) {
if (_.find(data.roles, _.matchesProperty("name", colDef.headerName))) {
this.setState({
value: true
});
}
}
}
updateGridAssociation(checked) {
const { data, colDef } = this.props;
// const { externalData, entitySpec, fieldSpec } = this.props.api;
// console.log(data);
// console.log(colDef);
if (checked) {
this.props.api.assign(data.id, colDef.id);
return;
}
this.props.api.unassign(data.id, colDef.id);
return;
}
handleCheckboxChange(event) {
const checked = !this.state.value;
this.updateGridAssociation(checked);
this.setState({ value: checked });
}
render() {
return (
<Checkbox
checked={this.state.value}
onChange={this.handleCheckboxChange}
/>
);
}
}
export default CheckboxItem;
Grid itself:
import React, { Component } from "react";
import { graphql, compose } from "react-apollo";
import gql from "graphql-tag";
import Grid from "#material-ui/core/Grid";
import _ from "lodash";
import { AgGridReact } from "ag-grid-react";
import { CheckboxItem } from "../Grid";
import "ag-grid/dist/styles/ag-grid.css";
import "ag-grid/dist/styles/ag-theme-material.css";
class UserRole extends Component {
constructor(props) {
super(props);
this.api = null;
}
generateColumns = roles => {
const columns = [];
const initialColumn = {
headerName: "User Email",
editable: false,
field: "email"
};
columns.push(initialColumn);
_.forEach(roles, role => {
const roleColumn = {
headerName: role.name,
editable: false,
cellRendererFramework: CheckboxItem,
id: role.id,
suppressMenu: true,
suppressSorting: true
};
columns.push(roleColumn);
});
if (this.api.setColumnDefs && roles) {
this.api.setColumnDefs(columns);
}
return columns;
};
onGridReady = params => {
this.api = params.api;
this.columnApi = params.columnApi;
this.api.assign = (userId, roleId) => {
this.props.assignRole({
variables: { userId, roleId },
refetchQueries: () => ["allUserRoles", "isAuthenticated"]
});
};
this.api.unassign = (userId, roleId) => {
this.props.unassignRole({
variables: { userId, roleId },
refetchQueries: () => ["allUserRoles", "isAuthenticated"]
});
};
params.api.sizeColumnsToFit();
};
onGridSizeChanged = params => {
const gridWidth = document.getElementById("grid-wrapper").offsetWidth;
const columnsToShow = [];
const columnsToHide = [];
let totalColsWidth = 0;
const allColumns = params.columnApi.getAllColumns();
for (let i = 0; i < allColumns.length; i++) {
const column = allColumns[i];
totalColsWidth += column.getMinWidth();
if (totalColsWidth > gridWidth) {
columnsToHide.push(column.colId);
} else {
columnsToShow.push(column.colId);
}
}
params.columnApi.setColumnsVisible(columnsToShow, true);
params.columnApi.setColumnsVisible(columnsToHide, false);
params.api.sizeColumnsToFit();
};
onCellValueChanged = params => {};
render() {
console.log(this.props);
const { users, roles } = this.props.userRoles;
if (this.api) {
this.api.setColumnDefs(this.generateColumns(roles));
this.api.sizeColumnsToFit();
this.api.externalData = roles;
this.api.setRowData(_.cloneDeep(users));
}
return (
<Grid
item
xs={12}
sm={12}
className="ag-theme-material"
style={{
height: "80vh",
width: "100vh"
}}
>
<AgGridReact
onGridReady={this.onGridReady}
onGridSizeChanged={this.onGridSizeChanged}
columnDefs={[]}
enableSorting
pagination
paginationAutoPageSize
enableFilter
enableCellChangeFlash
rowData={_.cloneDeep(users)}
deltaRowDataMode={true}
getRowNodeId={data => data.id}
onCellValueChanged={this.onCellValueChanged}
/>
</Grid>
);
}
}
const userRolesQuery = gql`
query allUserRoles {
users {
id
email
roles {
id
name
}
}
roles {
id
name
}
}
`;
const unassignRole = gql`
mutation($userId: String!, $roleId: String!) {
unassignUserRole(userId: $userId, roleId: $roleId) {
id
email
roles {
id
name
}
}
}
`;
const assignRole = gql`
mutation($userId: String!, $roleId: String!) {
assignUserRole(userId: $userId, roleId: $roleId) {
id
email
roles {
id
name
}
}
}
`;
export default compose(
graphql(userRolesQuery, {
name: "userRoles",
options: { fetchPolicy: "cache-and-network" }
}),
graphql(unassignRole, {
name: "unassignRole"
}),
graphql(assignRole, {
name: "assignRole"
})
)(UserRole);
I don't know ag-grid but ... in this case making requests results in entire grid (UserRole component) redraw.
This is normal when you pass actions (to childs) affecting entire parent state (new data arrived in props => redraw).
You can avoid this by shouldComponentUpdate() - f.e. redraw only if rows amount changes.
But there is another problem - you're making optimistic changes (change checkbox state) - what if mutation fails? You have to handle apollo error and force redraw of entire grid - change was local (cell). This can be done f.e. by setting flag (using setState) and additional condition in shouldComponentUpdate.
The best way for me to deal with this was to do a shouldComponentUpdate with network statuses in apollo, which took some digging around to see what was happening:
/**
* Important to understand that we use network statuses given to us by apollo to take over, if either are 4 (refetch) we hack around it by not updating
* IF the statuses are also equal it indicates some sort of refetching is trying to take place
* #param {obj} nextProps [Next props passed into react lifecycle]
* #return {[boolean]} [true if should update, else its false to not]
*/
shouldComponentUpdate = nextProps => {
const prevNetStatus = this.props.userRoles.networkStatus;
const netStatus = nextProps.userRoles.networkStatus;
const error = nextProps.userRoles.networkStatus === 8;
if (error) {
return true;
}
return (
prevNetStatus !== netStatus && prevNetStatus !== 4 && netStatus !== 4
);
};
It basically says if there is a error, just rerender to be accurate (and i think this ok assuming that errors wont happen much but you never know) then I check to see if any of the network statuses are not 4 (refetch) if they are I dont want a rerender, let me do what I want without react interfering at that level. (Like updating a child component).
prevNetStatus !== netStatus
This part of the code is just saying I want the initial load only to cause a UI update. I believe it works from loading -> success as a network status and then if you refetch from success -> refetch -> success or something of that nature.
Essentially I just looked in my props for the query and saw what I could work with.

Resources