How to separate UI and application state in Redux - reactjs

When writing a react-redux application, I need to keep both application and UI state in the global state tree. What is the best approach to design it's shape?
Lets say I have a list of todo items:
{
items: [
{ id: 1, text: 'Chew bubblegum' },
{ id: 2, text: 'Kick ass' }
]
}
Now I want to let the users select and expand the items. There are (at least) two options how to model the state shape:
{
items: [
{ id: 1, text: 'Chew bubblegum', selected: true, expanded: false },
{ id: 2, text: 'Kick ass', selected: false, expanded: false }
]
}
But this is mixing the UI state (selected and expanded) with the application state. When I save the todo list to the server, I want to save just the application state, not the UI state (in real-world app, UI state can contain state of modal dialogs, error messages, validation status etc).
Another approach is to keep another array for the UI state of the items:
{
items: [
{ id: 1, text: 'Chew bubblegum' },
{ id: 2, text: 'Kick ass' }
],
itemsState: [
{ selected: true, expanded: false },
{ selected: false, expanded: false }
]
}
Then you have to combine those two states when rendering an item. I can imagine that you can zip those two arrays in the connect function to make rendering easy:
const TodoItem = ([item, itemState]) => ...;
const TodoList = items => items.map(item => (<TodoItem item={item} />));
export default connect(state => _.zip(state.items, state.itemsState))(TodoList);
But updates to state can be painful, because items and itemsState must be kept in sync:
When removing an item, corresponding itemState must be removed.
When reordering items, itemsState must be reordered too.
When the list of todo items is updated from the server, it is necessary to keep ids in the UI state and do some reconciliation.
Is there any other option? Or is there any library that helps keeping the app state and UI state in sync?

Another approach inspired by normalizr:
{
ids: [12,11], // registry and ordering
data: {
11: {text: 'Chew bubblegum'},
12: {text: 'Kick ass'}
},
ui: {
11: { selected: true, expanded: false },
12: { selected: false, expanded: false }
}
}

I'm currently looking at this myself for a side project. I'm going to approach it similar to Rick's method above. The data{} serves as the source of truth and you use that to push local ui changes into (reflecting the most current state). You do need to merge the data and ui together before render, and I myself have tried that in a few places. I will say, as far as keeping in sync, it shouldn't be too bad. Basically, whenever you save/fetch data, you're updating data{} and clearing out ui{} to prepare for the next use case.

Related

What is the proper way to update multiple objects in an array of objects in react?

Making a watchlist app for stocks/crypto to learn react.
I have watchlist state in this format (using useState hook):
[
{ id: "1btc", name: "bitcoin", price: "7500" },
{ id: "1eth", name: "ethereum", price: "500" },
{ id: "2xmr", name: "monero", price: "200" },
{ id: "1ltc", name: "litecoin", price: "10" },
];
every 3 seconds server sends a batch of available price updates over websocket connection.
sometimes only a handful of coins have new info so update message might look like so:
[
{ id: "2xmr", price: "225" },
{ id: "1btc", price: "8600" },
];
Is it possible to update the watchlist so that only the updated coins in the list rerender as opposed to rerendering the entire list every time an update message is received? What is the best way to handle this situation?
You could let each object have its own component and then pass the object properties in the components and then use React.memo() so that it would only cause a render if it detects a change.
By default it will only shallowly compare the objects in the props which should be enough for you. But if its more complex, then you can also write a custom function.
As per docs
function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export default React.memo(MyComponent, areEqual);

componentDidUpdate is one step behind the actual value in the setState

I'm using React and chartJs to create some basic charts for a dashboard.
I want to implement a onClick functionality which will take the clicked element's data and update it in a AgGrid table.
I managed to do that but it appears that I have one problem: Whenever I click one element, it updates the values from the previous clicked element.
I understand that it has to do with the fact that setState is async and does not update at the moment when I click an element.
Any ideas?
below is my code:
handleChartClick = (element) => {
if (element[0] !== undefined) {
const { datasets } = element[0]._chart.tooltip._data;
const datasetIndex = element[0]._datasetIndex;
const dataIndex = element[0]._index;
const dataLabel = element[datasetIndex]._chart.data.labels[dataIndex];
const value = datasets[datasetIndex].data[dataIndex];
//alert(`${dataLabel}: ${value}`);
this.setState({
tabledata: {
columnDefs: [{ headerName: 'Service name', field: 'service', sortable: true, filter: true, resizable: true },
{ headerName: 'Running times', field: 'times', sortable: true, filter: true, resizable: true }],
rowData: [{ service: dataLabel, times: value }]
}
}, () => {});
}
}
In the Child class:
componentDidUpdate() {
this.state.tabledata = this.props.tabledata;
}
Extra: I am using classes for the definition of chart Components and the App.
/// Later Edit:
Found the problem, when i was instantiating my AgGrid, instead of using this.props.smth, I was using the this.state.smth (the problem was that at the current point, the state has not been modified yet and it was referencing to old values).

React - setting state with imported data keeps getting updated

I am importing an array of data like
import { menuData } from '../../data/menu-data';
data being:
export const menuData = [
{ id: 'all', title: 'Select all', icon: '', selected: false },
{ id: 'overview', title: 'Overview', icon: 'user-check', selected: false },
{
id: 'about',
title: 'About',
icon: 'info-circle',
selected: false,
partSelected: false,
}
];
I then initialise some state in my parent component like:
const [downloadMenu, setMenuState] = useState([...menuData]);
I pass "downloadMenu" state to my child compionent and absorb it as a prop and then create a copy of it which gets mutated i.e.
const updatedMenu = [...downloadMenu];
i then call this function which is in the parent and is passed down as a prop to the child component
onMenuChange(updatedMenu);
const handleMenuChange = (menuState) => {
setMenuState(menuState)
}
This works but when i try and reset the state to the initial menuData it doesnt work. MenuData is getting mutated aswell. Why is this?
I am calling this again - setMenuState([...menuData]);
but menuData is the same as the downloadMenu state.
Using ... only creates a shallow copy - a new list is created, but the items inside it remain the same. The objects in the list aren't copied, but remain as a reference to the original objects in menuData.
The solution is either to create a new object when you change menuData or create a deep copy.
A deep copy can be created using JSON - JSON.parse(JSON.stringify(menuData)) or a library like lodash - _.cloneDeep(menuData).

react how to prevent default on setState without event

Let's say you have a defined function containing a this.setState in a react component which is not fired by an event.
How can you do preventDefault() in order to keep the current scroll on the page?
I create a sandbox to illustrate this behaviour:
https://codesandbox.io/embed/damp-night-r92m5?fontsize=14&hidenavigation=1&theme=dark
When groups are defined and something fire the renderer, the page scroll on top. This does not happend id groups are not defined or contain an empty array...
How can I prevent this scrolling
First of all, I think your issue was nothing to do with event but your component is triggering render.
Based on your sandbox that you provided, you are actually declaring a new array each time the component renders. Meaning that React will assume that your [{ id: 1, content: "group1" }, { id: 2, content: "group2" }] is a new instance, even though all the items in the array is the same.
This line is causing your issue :
groups={[{ id: 1, content: "group1" }, { id: 2, content: "group2" }]}
Method 1: Move your groups variables into state
const [groups, setGroups] = useState([{ id: 1, content: "group1" }, { id: 2, content: "group2" }]);
This way React will not rerender your App until you call setState ( In this case, setGroups )
Method 2: Move your groups outside of your App function
const groups = [{ id: 1, content: "group1" }, { id: 2, content: "group2" }];
function App() {
... App Codes
}
In this way, React will not rerender your App since groups is not declaring within App.
Method 3: Using Memoization useMemo React Hook
const groups = useMemo(() => [{ id: 1, content: "group1" }, { id: 2, content: "group2" }], []);
The second argument in your useMemo function defines your dependencies array, setting it to empty array means that the value will never change. Hence React will not rerender your App.
In your render:
groups={groups}

React Redux Set Item Complete

I have a feeling I'm so close to this... I have a to do list application I'm building.
A user can click on a button labeled "complete" to mark that specific item as complete.
My thinking on this is, when the user clicks that button to only update "completed" state to true.
For some reason the text within the state changed to "undefined" from the selected item and another item adds to the state when clicking "complete"
Here is my action:
export function completeTodo() {
return {
type: "COMPLETE_TODO",
completed: true
}
}
Here is my reducer:
case "COMPLETE_TODO": {
return Object.assign({}, state, {
todos: [{
text: action.text,
completed: action.completed,
id: getId(state)
}, ...state.todos]
});
}
The following code creates new array with one new todo object and all the previous todos (so basically you are adding one todo to the beginning of the array from previous state):
[
{
text: action.text,
completed: action.completed,
id: getId(state)
},
...state
]
You should filter out old todo object:
[
{
text: action.text,
completed: action.completed,
id: getId(state)
},
...state.todos.filter(todo => todo.id !== getId(state))
]
Your COMPLETE_TODO action doesn't have a text field so the reducer assigns undefined to the state.
As far as I understand, you dont want to change the items' text property when completed. You can remove the text assignment from the reducer.

Resources