Where should I handle sorting in Redux App? - reactjs

I have an action / reducer / components. In one of my components (component dump) I have a Select. I get information on what type of filter my store. Where can I handle it in action, or reducer?

IMO, the right place to sort data is not directly in the reducers but in the selectors.
From redux docs:
Computing Derived Data
Reselect is a simple library for creating memoized, composable selector functions. Reselect selectors can be used to efficiently compute derived data from the Redux store.
I'm currently using selectors to filter and sort data.
No data repetition in the state. You don't have to store a copy of the item sorted by one specific way.
The same data can be used in different components, each one using a different selector function to sort for example.
You can combine selector applying many data computations using selector that you already have in the application.
If you do right, your selectors will be pure functions, then you can easily test them.
Use the same selector in many components.

I save the items, sortKey and sortKind (asc/desc) in the Redux Store.
In my Angular component (I believe would be same for React), I get the store state as an Observable so that I can display the items, sortKey and sortOrder in the UX.
When the user clicks on the table columns to change sort key (order) I dispatch the new keys/sort order to reducer for the state.
The reducer then performs the new sorting, and returns the new state with the updated values.
The Observable in the component thus sparks an event which updates the UX.
Advantage:
keep sorting logic out of the component
by saving the sortKey and sortKind in the state, you can restore precisely the UX if the user refreshes the browser (I use Redux-LocalStorage to sync)
as the store has the sorted items, you'll only perform sorting when the user actively wants it.
the sorted items are remembered for when the user might return to the component.
My reducer ( "bizzes" is my items list, and I use Immutable.List to store the items)
import { List } from 'immutable';
import { IBizz, IBizzState } from './bizz.types';
import { BIZZES_SET, BIZZES_SORT} from 'store/constants';
const SORT_ASC = 'asc';
const SORT_DESC = 'desc';
const defaultSortKey = 'serialNo';
const defaultSortOrder = SORT_ASC;
const INITIAL_STATE: IBizzState = {
bizzes: List([]),
sortKey: defaultSortKey,
sortOrder: defaultSortOrder
};
export function bizzReducer(state: IBizzState = INITIAL_STATE, action: any): IBizzState {
switch (action.type) {
case BIZZES_SET:
return {
bizzes: List(action.payload.bizzes),
sortKey: action.payload.sortKey || defaultSortKey,
sortOrder: action.payload.sortOrder || defaultSortOrder
};
case BIZZES_SORT:
let sortKey = action.payload.sortKey || defaultSortKey;
if(sortKey === state.sortKey) {
state.sortOrder = state.sortOrder === SORT_ASC ? SORT_DESC : SORT_ASC;
}
return {
bizzes: List(state.bizzes.sort( (a, b) => {
if( a[sortKey] < b[sortKey] ) return state.sortOrder === SORT_ASC ? -1 : 1;
if( a[sortKey] > b[sortKey] ) return state.sortOrder === SORT_ASC ? 1: -1;
return 0;
})),
sortKey: sortKey,
sortOrder: state.sortOrder
};
default: return state;
}
}
And my component ( I use Ng2-Redux to get the store as Observables):
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '#angular/core';
import { select } from 'store';
import { BizzActions } from 'actions/index';
#Component({
selector: 'bizzlist',
templateUrl: './bizz-list.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BizzListComponent implements OnInit {
#select([ 'bizzState']) bizzState$;
public sortOrder: string;
public sortKey: string;
public bizzes = [];
private bizzStateSubscription;
constructor(
public bizzActions: BizzActions
) { }
ngOnInit() {
this.bizzStateSubscription = this.bizzState$.subscribe( bizzState => {
this.bizzes = bizzState.bizzes;
this.sortKey = bizzState.sortKey;
this.sortOrder = bizzState.sortOrder;
});
}
ngOnDestroy() {
this.bizzStateSubscription.unsubscribe();
}
public sortBizzes(key) {
this.bizzActions.sortBizzes(key);
}
}
As you can see, I am using an Action (called BizzActions) to do the actual Redux dispatch. You could do it in your component, but I prefer to separate these things. For good measure, here's my BizzActions (a Service):
import { Injectable } from '#angular/core';
import { NgRedux, IAppState } from 'store';
import {
BIZZES_SET,
BIZZES_SORT
} from 'store/constants';
#Injectable()
export class BizzActions {
constructor (private ngRedux: NgRedux<IAppState>) {}
public setBizzes = (bizzes: any) => {
return this.ngRedux.dispatch({
type: BIZZES_SET,
payload: {
bizzes: bizzes
}
});
};
public sortBizzes = (key:string) => {
return this.ngRedux.dispatch({
type: BIZZES_SORT,
payload: {
sortKey: key
}
});
};
}

You could sort the data when #connect -ing your React component with the Redux store:
function mapStateToProps(state) {
var items = state.items.slice(0);
items.sort()
return {
items: items
}
}
#connect(mapStoreToProps)
class MyComponent extends React.Component {
render() {
var items = this.props.items;
}
}
The Redux documentation shows a similar case in the Todo example: https://redux.js.org/basics/usage-with-react

I've been sorting my reducers using a section dictionary pattern. In other words, I sort my items by headers, say a date, and then store the objects in arrays by the date key:
sectionHeaders: ["Monday", "Tuesday"],
dict:{
Monday: [{obj1},{obj2},{obj3}],
Tuesday: [{obj4}],
}
Then I use this dict in React Native to populate my ListView because ListView will except this object format to render Items with Sections using the cloneWithRowsAndSections method.
This is a performance optimization because my sorting is not trivial. I have to make deep comparisons and this way I only do it once when I first populate the store, and not every time I render the scene.
I've also played around with using a dictionary by ID and storing only the IDs in the sorted dict instead of the actual objects.
There are trade offs though for this, in that updating is more complex and you have to decide when to remove section headers if an item is removed from a section.

Related

React-component is not re-rendered when the store is changed, neither automatically nor even by force update

This functional component should display a sorted list with checkboxes at each item that change the values in the store.
For some reason it is not re-rendered when the store is changed. And without a re-renderer, it (and the whole application) works very crookedly and halfway. I suspect that this is because the store object remains the same, albeit with new content. But I don’t understand how to fix it. I have even inserted a force update to the checkbox handler, but for some reason it does not work too.
Component:
import React, { useState, useReducer } from 'react';
import { ReactSortable } from 'react-sortablejs';
import ListItem from '#mui/material/ListItem';
import Checkbox from '#mui/material/Checkbox';
import { connect } from 'react-redux';
import { setGameVisible, setGameInvisible } from '../store/actions/games';
interface IGamesListProps {
games: [];
setGameVisible: (id: string) => void;
setGameInvisible: (id: string) => void;
}
interface ItemType {
id: string;
name: string;
isVisible: boolean;
}
const GamesList: React.FunctionComponent<IGamesListProps> = ({games, setGameVisible, setGameInvisible}) => {
const [state, setState] = useState<ItemType[]>(games);
// eslint-disable-next-line
const [ignored, forceUpdate] = useReducer(x => x + 1, 0); // this way of force updating is taken from the official React documentation (but even it doesn't work!)
const onCheckboxChangeHandle = (id: string, isVisible: boolean) => {
isVisible ? setGameInvisible(id) : setGameVisible(id);
forceUpdate(); // doesn't work :(((
}
return (
<ReactSortable list={state} setList={setState} tag='ul'>
{state.map((item) => (
<ListItem
sx={{ maxWidth: '300px' }}
key={item.id}
secondaryAction={
<Checkbox
edge="end"
onChange={() => onCheckboxChangeHandle(item.id, item.isVisible)}
checked={item.isVisible}
/>
}
>
{item.name}
</ListItem>
))}
</ReactSortable>
);
};
export default connect(null, { setGameVisible, setGameInvisible })(GamesList);
Reducer:
import { SET_GAMES, SET_GAME_VISIBLE, SET_GAME_INVISIBLE } from '../actions/games';
export const initialState = {
games: [],
};
export default function games(state = initialState, action) {
switch(action.type) {
case SET_GAMES: {
for(let obj of action.payload.games) {
obj.isVisible = true;
}
return {
...state,
games: action.payload.games,
};
}
case SET_GAME_VISIBLE: {
for(let obj of state.games) {
if (obj.id === action.payload.id) {
obj.isVisible = true;
};
}
return {
...state,
};
}
case SET_GAME_INVISIBLE: {
for(let obj of state.games) {
if (obj.id === action.payload.id) {
obj.isVisible = false;
};
}
return {
...state,
};
}
default:
return state;
}
}
Thank you for any help!
Note: By the information You gave I came with the idea of the problem, but I posted here because it is going to be explanatory and long.
First of all, you don't pass the new game via mapStateToProps into Component in a state change, and even you do, useState won't use new game prop value for non-first render. You must use useEffect and trigger changes of the game and set the to state locally.
At this point you must find the inner state redundant and you can remove it and totally rely on the redux state.
const mapStateToProp = (state) => ({
games: state.games // you may need to change the path
})
connect(mapStateToProp, { setGameVisible, setGameInvisible })(GamesList);
Second, the reducer you made, changes the individual game item but not the games list itself. because it is nested and the reference check by default is done as strict equality reference check-in redux state === state. This probably doesn't cause an issue because the outer state changes by the way, but I think it worth it to mention it.
for(let obj of action.payload.games) {
obj.isVisible = true; // mutating actions.payload.games[<item>]
}
return {
...state,
games: [...action.payload.games], // add immutability for re-redenr
};
// or use map
return {
...state,
games: action.payload.games.map(obj => ({...obj, isVisible:true})),
};
Third, It's true your forceUpdate will cause the component to re-render, and you can test that by adding a console.log, but it won't repaint the whole subtree of your component including inner children if their props don't change that's because of performance issue. React try to update as efficiently as possible. Also you the key optimization layer which prevent change if the order of items and id of them doesn't change

Central State Management without Redux or Mobx?

Recently I contemplated the idea of having central state management in my React apps without using Redux or Mobx, instead opting to create something similar to the application class in Android. In any event, I implemented something similar to this:
Create a store folder and a file called store.js in it whose contents are:
// State
let state = {
users: {},
value: 0
};
// Stores references to component functions
let triggers = [];
// Subscription Methods
export const subscribe = trigger => {
triggers.push(trigger);
trigger();
}
export const unsubscribe = trigger => {
let pos = -1;
for (let i in triggers) {
if (triggers[i]===trigger) {
pos = i;
break;
}
}
if (pos!==-1) {
triggers.splice(pos, 1);
}
}
// Trigger Methods
let triggerAll = () => {
for (let trigger of triggers) {
trigger();
}
}
// State Interaction Methods
export const setUser = (name, description) => {
state.users[name] = description;
triggerAll();
}
export const removeUser = name => {
if (name in state.users) {
delete state.users[name];
}
triggerAll();
}
export const getAllUsers = () => {
return state.users;
}
export const getUser = name => {
if (!(name in state.users)) {
return null;
}
return state.users[name];
}
export const getValue = () => {
return state.value;
}
export const setValue = value => {
state.value = value;
triggerAll();
}
And connecting to this store in the following manner:
// External Modules
import React, { Component } from 'react';
import {Box, Text, Heading} from 'grommet';
// Store
import {subscribe, unsubscribe, getAllUsers} from '../../store/store';
class Users extends Component {
state = {
users: []
}
componentDidMount() {
subscribe(this.trigger); // push the trigger when the component mounts
}
componentWillUnmount() {
unsubscribe(this.trigger); // remove the trigger when the component is about to unmount
}
// function that gets triggered whenever state in store.js changes
trigger = () => {
let Users = getAllUsers();
let users = [];
for (let user in Users) {
users.push({
name: user,
description: Users[user]
});
}
this.setState({users});
}
render() {
return <Box align="center">
{this.state.users.map(user => {
return <Box
style={{cursor: "pointer"}}
width="500px"
background={{color: "#EBE7F3"}}
key={user.name}
round
pad="medium"
margin="medium"
onClick={() => this.props.history.push("/users/" + user.name)}>
<Heading margin={{top: "xsmall", left: "xsmall", right: "xsmall", bottom: "xsmall"}}>{user.name}</Heading>
<Text>{user.description}</Text>
</Box>
})}
</Box>;
}
}
export default Users;
Note. I've tested this pattern on a website and it works. Check it out here. And I apologize I am trying to keep the question concise for stackoverflow, I've provided a more detailed explanation of the pattern's implementation here
But anyway, my main question, what could be the possible reasons not to use this, since I assume if it was this simple, people wouldn't be using Redux or Mobx. Thanks in advance.
That's what Redux and MobX basically do, you are wrong in thinking that at their core concept they are much different. Their size and complexity came as a result of their effort to neutralize bugs and adapt to a vast variety of application cases. That's it. Although they might be approaching the task from different angles, but the central concept is just that. Maybe you should familiarize your self with what they actually do underneath.
Btw, you do not need to store redundant state in your component, if all you need is to trigger the update. You can just call forceUpdate() directly:
// function that gets triggered whenever state in store.js changes
trigger = () => {
this.forceUpdate();
}
That's similar to what Redux and MobX bindings for react do under the hood.

Is that ok having nested observables in mobx?

I'm new to Mobx and having some troubles with it.
Is that ok having an observable inside another observable in Mobx?
I have a store:
class ObservableTasksStore {
#observable
tasks = [
new Task(111, 'clean the car', 'with soap plz, it is really important', Duration(7, TIME.DAYS), Duration(6, TIME.MONTHS), '2018-08-17T10:02:18.674Z'),
new Task(222, 'check car wheels', '', Duration(2, TIME.DAYS), Duration(1, TIME.MONTHS)),
new Task(333, 'shinanint', ' check your teeth', Duration(2, TIME.DAYS), Duration(1, TIME.MONTHS))
]
#action
markTaskAsDone = (id) => {
const index = this.tasks.findIndex((_tsk) => _tsk.id === id)
this.tasks[index].markAsDone()
}
but it doesn't call render on change.
the class to be rendered:
import React from 'react'
import { TaskCard } from '../TaskCard/TaskCard'
import { observer } from 'mobx-react'
const TasksList = ({taskStore}) => {///ObservableTasksStore passed as arg
const tasks = taskStore.tasks
return (
tasks.map((task) => {
return <TaskCard key={task.id}
id={task.id}
taskName={task.taskName}
description={task.description}
notifyDuration={task.notifyDuration}
recurring={task.recurring}
lastDone={task.lastDone}
daysLeft={task.daysLeft}
dueDate={task.dueDate}
/>
})
)
}
export default observer(TasksList)
only when I add #observable to the Task class.
1.is that Ok that Task also has #observable??
export class Task {
id;
taskName;
description;
notifyDuration;
recurring;
#observable
lastDone;
#observable
isActive;
constructor (id, taskName, description, notifyDuration, recurring, lastDone, isActive) {
this.id = id || uuidv4()
this.taskName = taskName
this.description = description
this.notifyDuration = notifyDuration
this.recurring = recurring
this.lastDone = lastDone ? moment(lastDone) : undefined
this.isActive = isActive || true
}
#action
markAsDone () {
this.lastDone = moment()
this.isActive = false
}
}
the thing is that when I'm adding a mobx strict mode I have a Maximum call stack size exceeded
index.module.js:768 Uncaught RangeError: Maximum call stack size exceeded
at initializeInstance$$1 (mobx.module.js:275)
at ObservableTasksStore.get [as tasks] (mobx.module.js:266)
any idea why?
the source can be found here:
https://github.com/yuriabaev/taskScheduler/tree/mobx-nested-observable
For your case you must use #observable in Task class, because when you create an object from a class only the properties that you specify to be observables are observables (even if you push them in observable array).
To fix the issue you should enforce actions like that:
configure({
enforceActions: true
});
from mobx version 5.1.0 you should use it like that:
configure({
enforceActions: 'observed'
});
because you're update an observable value in the constructor, for more information: https://mobx.js.org/refguide/api.html#-enforceactions.

reactjs and redux: How to implement filter text box for a rows of data

I am learning reactj and redux. Somehow i managed to get data from api and display them in table. The table is shown below.
I want to add filter text box below each column header so that when i type text, it shows those values matching the results.
What is the flow using redux. Or any example
I just typed this up, its just for a general idea of how to filter data in your mapstate function. This is called computing derived data https://redux.js.org/docs/recipes/ComputingDerivedData.html and its good to use tools like Reselect for performance benefits. This isnt working code, but its just to give you an idea of how it is usually done so you dont get duplicate state in your reducers.
class Table extends React.Component {
filterData = (event, column) => {
this.props.someFilterAction(event.target.value, column)
}
renderItem = (item) => {
return <someItemDiv searchAction={(e) => this.filterData(e, item.column) } />
}
render(){
<div>
{ this.props.data.map(this.renderItem) }
</div>
}
}
function mapStateToProps(state) {
const searchItem = state.searchData;
// reselect is good for stuff like this
const filteredData = state.tableData.filter(item => item[searchItem.column].text.indexOf(searchItem.input) !== -1 )
// or some kind of search criteria you want
return {
data: filteredData,
searchItem: state.searchData
}
}
const mapDispatchToProps = {
someFilterAction: YourReduxActionCreator
}
export default connect(mapStateToProps, mapDispatchToProps)(Table);
function YourReduxActionCreator(input, column) {
return {
type: FILTER_DATA,
payload: {
column,
input,
}
}
}
function yourReducer() {
switch(state, action) {
case FILTER_DATA:
return action.payload
default:
return state;
}
}
Hey you just need to create 2 selectors.
The first one : getTableData() that get your whole table data from your store
Then getFilteredTableData() which will filter the result of getTableData() using the form values of your field (your keyword and the column).
The selector getFilteredTableData() should be the source of data for your table.
So as soon as your form change your table will be refiltered !

React Redux, how to properly handle changing object in array?

I have a React Redux app which gets data from my server and displays that data.
I am displaying the data in my parent container with something like:
render(){
var dataList = this.props.data.map( (data)=> <CustomComponent key={data.id}> data.name </CustomComponent>)
return (
<div>
{dataList}
</div>
)
}
When I interact with my app, sometimes, I need to update a specific CustomComponent.
Since each CustomComponent has an id I send that to my server with some data about what the user chose. (ie it's a form)
The server responds with the updated object for that id.
And in my redux module, I iterate through my current data state and find the object whose id's
export function receiveNewData(id){
return (dispatch, getState) => {
const currentData = getState().data
for (var i=0; i < currentData.length; i++){
if (currentData[i] === id) {
const updatedDataObject = Object.assign({},currentData[i], {newParam:"blahBlah"})
allUpdatedData = [
...currentData.slice(0,i),
updatedDataObject,
...currentData.slice(i+1)
]
dispatch(updateData(allUpdatedData))
break
}
}
}
}
const updateData = createAction("UPDATE_DATA")
createAction comes from redux-actions which basically creates an object of {type, payload}. (It standardizes action creators)
Anyways, from this example you can see that each time I have a change I constantly iterate through my entire array to identify which object is changing.
This seems inefficient to me considering I already have the id of that object.
I'm wondering if there is a better way to handle this for React / Redux? Any suggestions?
Your action creator is doing too much. It's taking on work that belongs in the reducer. All your action creator need do is announce what to change, not how to change it. e.g.
export function updateData(id, data) {
return {
type: 'UPDATE_DATA',
id: id,
data: data
};
}
Now move all that logic into the reducer. e.g.
case 'UPDATE_DATA':
const index = state.items.findIndex((item) => item.id === action.id);
return Object.assign({}, state, {
items: [
...state.items.slice(0, index),
Object.assign({}, state.items[index], action.data),
...state.items.slice(index + 1)
]
});
If you're worried about the O(n) call of Array#findIndex, then consider re-indexing your data with normalizr (or something similar). However only do this if you're experiencing performance problems; it shouldn't be necessary with small data sets.
Why not using an object indexed by id? You'll then only have to access the property of your object using it.
const data = { 1: { id: 1, name: 'one' }, 2: { id: 2, name: 'two' } }
Then your render will look like this:
render () {
return (
<div>
{Object.keys(this.props.data).forEach(key => {
const data = this.props.data[key]
return <CustomComponent key={data.id}>{data.name}</CustomComponent>
})}
</div>
)
}
And your receive data action, I updated a bit:
export function receiveNewData (id) {
return (dispatch, getState) => {
const currentData = getState().data
dispatch(updateData({
...currentData,
[id]: {
...currentData[id],
{ newParam: 'blahBlah' }
}
}))
}
}
Though I agree with David that a lot of the action logic should be moved to your reducer handler.

Resources