Updating state based on selection (Redux) - reactjs

being new to redux, I came across the following challenge: I have a filter-list, which must be updated both after the first load of a component, and every time someone changes a dropdown in the top navbar. The filter-list is dependent on another dropdown which is part of my state. A pseudocode is below [1].
I could theoretically duplicate my action dispatch in both Components (the main one and the navbar), but this is bad design of course.
This brings me to the question: Where does one put redux dispatch calls which must access the state themselves? Would this live in the action-file which would then need to access the state itself (which might not be good design on its own)?
Thanks!
[1]
function getDrillOptions() {
let data = props.data; // <- is part of redux' store
let countries = props.selectedFilters.countries; //<- part of redux' store
let regions = getRegionForCountry(countries);
let selectable = new Set();
Object.keys(data[regions][countries]).map((weeknum) => {
data[regions][countries][weeknum].map((row) => {
selectable.add(row[event.target.value]);
});
});
dispatch(populateFilters({filter: 'mainDrillOptions', value: [...selectable]})); // <- sets state
}

Related

Create and Read State for thousands of items using Recoil

I've just started using Recoil on a new project and I'm not sure if there is a better way to accomplish this.
My app is an interface to basically edit a JSON file containing an array of objects. It reads the file in, groups the objects based on a specific property into tabs, and then a user can navigate the tabs, see the few hundred values per tab, make changes and then save the changes.
I'm using recoil because it allows me to access the state of each input from anywhere in my app, which makes saving much easier - in theory...
In order to generate State for each object in the JSON file, I've created an component that returns null and I map over the initial array, create the component, which creates Recoil state using an AtomFamily, and then also saves the ID to another piece of Recoil state so I can keep a list of everything.
Question 1 Is these a better way to do this? The null component doesn't feel right, but storing the whole array in a single piece of state causes a re-render of everything on every keypress.
To Save the data, I have a button which calls a function. That function just needs to get the ID's, loop through them, get the state of each one, and push them into an Array. I've done this with a Selector too, but the issue is that I can't call getRecoilValue from a function because of the Rules of Hooks - but if I make the value available to the parent component, it again slows everything right down.
Question 2 I'm pretty sure I'm missing the right way to think about storing state and using hooks, but I haven't found any samples for this particular use case - needing to generate the state up front, and then accessing it all again on Save. Any guidance?
Question 1
Get accustomed to null-rendering components, you almost can't avoid them with Recoil and, more in general, this hooks-first React world 😉
About the useRecoilValue inside a function: you're right, you should leverage useRecoilCallback for that kind of task. With useRecoilCallback you have a central point where you can get and set whatever you want at once. Take a look at this working CodeSandbox where I tried to replicate (the most minimal way) your use-case. The SaveData component (a dedicated component is not necessary, you could just expose the Recoil callback without creating an ad-hoc component) is the following
const SaveData = () => {
const saveData = useRecoilCallback(({ snapshot }) => async () => {
const ids = await snapshot.getPromise(carIds);
for (const carId of ids) {
const car = await snapshot.getPromise(cars(carId));
const carIndex = db.findIndex(({ id }) => id === carId);
db[carIndex] = car;
}
console.log("Data saved, new `db` is");
console.log(JSON.stringify(db, null, 2));
});
return <button onClick={saveData}>Save data</button>;
};
as you can see:
it retrieves all the ids through const ids = await snapshot.getPromise(carIds);
it uses the ids to retrieve all the cars from the atom family const car = await snapshot.getPromise(cars(carId));
All of that in a central point, without hooks and without subscribing the component to atoms updates.
Question 2
There are a few approaches for your use case:
creating empty atoms when the app starts, updating them, and saving them in the end. It's what my CodeSandbox does
doing the same but initializing the atoms through RecoilRoot' initialState prop
being updated by Recoil about every atom change. This is possible with useRecoilTransactionObserver but please, note that it's currently marked as unstable. A new way to do the same will be available soon (I guess) but at the moment it's the only solution
The latter is the "smarter" approach but it really depends on your use case, it's up to you to think if you really want to update the JSON at every atom' update 😉
I hope it helps, let me know if I missed something 😊

Is it possible to save components state when they are stored in an array manipulated?

I'm trying to create a stepper form
I store my steps in an array of json with a proprety component ({typeOfComponent, component, key})
It works wells, but:
Everytime i slice my array, like when i move up/down a step or add a new step between two steps.
I lose the states inside my component.
I tried to use memo, i don't understand why it's only when an item position my composent is recreate. Is it possible like a pointer in C to store only his "adress"
the code sandbox exemple =>
https://codesandbox.io/s/infallible-maxwell-zkwbm?file=/src/App.js
In my real projet, the button ADD is a button for chosing the new step type
Is there any solution for manipulates my steps without losing the user data inside ?
Thanks for your help
React is re-mounting the components inside of this every re-render probably due to a variety of reasons. I couldn't get it to work as is, but by lifting the state up from your components, it will work.
You'd likely need to lift the state up anyway because the data isn't where you need it to be to make any use of your form when the user is done with it.
In order to lift the state up, I added the current value to the steps array:
function addNext(step, index) {
componentKey++;
setSteps(prevState => {
let newState = [...prevState];
step = 1;
newState.splice(index + 1, 0, {
stepNumber: step,
component: getStepContent(step, componentKey),
value: getDefaultValue(step),
key: componentKey
});
return newState;
});
}
I also made sure your getStepContent just returned the component rather than a node so you can render it like this:
<step.component
value={step.value}
onChange={handleChange}
data-index={i}
/>
There are definitely a lot of ways to optimize this if you start running into performance issues, of course.
https://codesandbox.io/s/beautiful-river-2jltr?file=/src/App.js

Recording redux navigation state in model state?

I've structured my redux application such that my data models are handled on separate branches of the state tree.
{concerts, venues}
I've also used react-navigation-redux-helpers to put my navigation state into the tree:
{concerts, venues, nav}
However, I want to record information about the visibility state of a particular model. When the ConcertScreen is shown, I want to know when a user's looking/stops looking at a particular concert ID (and letting the server know), with the eventual goal of measuring how long a particular concert ID was visible on screen.
I've done this by adding branches for Navigation/NAVIGATE, Navigation/RESET, and Navigation/BACK to the concerts reducer, and setting visible: true on the appropriate object under concerts.
This has been error prone, since the navigation state can be modified by actions other than these specific actions. (A logout action handled directly by the nav reducer, for example.)
I see two alternatives, both not ideal:
Use props.navigation.addListener to listen to focus and blur events on the ConcertScreen, trigger custom concertFocused/concertBlurred actions, and handle those in my concert reducer instead of the Navigation/* actions.
Create a selector that computes the currently visible concert from the nav state and refactor the business logic that expects concert.visible as input to use the selector instead.
The problem with 1 seems to be that it's adding overhead to the event loop, all the extra actions flying around means extra rendering overhead.
2 avoids the extra actions, but it seems like a lot of refactoring for not a whole lot of gain, and it means I have to move business logic out of the concert reducer and put it elsewhere.
Say I use option 2. I add a middleware that, on any action, applies the selector to state.nav and from that computes what Concert is currently displayed. If I wanted to measure duration, how would I store start/end time? Fire a new action with that added data so the concert reducer catches it? That just seems like option 1 with extra steps.
I could also have this middleware add a field to every action indicating the concert display state, and have the concert reducer handle it in the default/fallthrough case. Do people do that?
I would approach your use-case in such a way, that I will get the best of both solutions.
First of all, having many actions dispatched you're worried about rendering overhead. Using a selector library, let's say reselect, the library memoization will prevent unnecessarily components rerendering.
Later on, if I understand you correctly, your goal is to let the server know the visibility status of an item (concert) and eventually its visible time. If your goal is notifying the server only, without letting know the rest app's front-end users, why do you want to keep tracking it in Redux too? You can skip the Redux part and send updates to the server only.
Let's assume, that you need Redux for the tracking. You can try on your way structuring the Store, as you already mentioned, adding the visible flag to each object in the Redux store. But if your items' structure is bigger enough and it's costly to copy and update the object each time when changing the visible flag, you can consider creating a dedicated Store branch and reducer, that will be responsible only for the tracking needs. Something like that:
tracking : {
concerts: {
1: { visible: true, time: 10 }
}
}
Now, updating an item's flag, only the above tiny structure has to be copied and modified. Even, you can make it smaller and more specific for a certain item type (trackingConcerts).
* Keep in mind, it's on your own to decide whether or not such a dedicated Store branch will improve the performance, because we don't know your detailed architecture and Store specifics.
Continuing with the solutions ...
Relying on navigation actions + middleware is error prone, as you mentioned. What about the use-case you have a general page of components (i.e. navigation action with generic name will be dispatched), but you render there one of your items (concert)? Also rendering an item, would be always coupled with modifying the mapping logic in your middleware or wherever place you track the items by action name. Another tricky case is when you render different type of items (concerts, venues) on exactly one page. How will you differ and track the items, considering you have only 1 navigation item? Also in a such setup, I don't see a straightforward way for handling an item visible time.
About the selectors as a solution - they can be only a small part of the solution. The selector is responsible for selecting and managing derived state. Nothing more.
Show me the code, please.
I would create a wrapper component around react on screen (or any similar library that tracks component visibility) and implement only the tracking visible time of the component.
The wrapper will trigger callbacks when the component visibility state is changed and a callback on componentDidUnmount including the visible time.
That's all! Now you can attach handlers on these callbacks and you can update your Redux and/or notify the server for the visibility changes, without relying on any navigation actions and middlewares.
Usage:
const App = () => (
<Tracking
onVisibilityChange={isVisible => {}}
onUnmount={visibleSeconds => {}}
>
<Concert id={1} />
</Tracking>
)
Tracking Wrapper:
import TrackVisibility from 'react-on-screen'
const Tracking = ({ children, libraryProps, ...rest }) => (
<TrackVisibility {...libraryProps}>
<TrackingCore {...rest}>
{children}
</TrackingCore>
</TrackVisibility>
)
TrackingCore, our custom tracking logic:
class TrackingCore extends React.Component {
constructor (props) {
super(props)
this.state = {
visibleSeconds: 0,
interval: null
}
}
componentDidMount() {
this.track()
}
componentWillReceiveProps (nextProps) {
this.track(nextProps)
}
componentDidUnmount() {
const { visibleSeconds, interval } = this.state
const { onUnmount } = this.props
onUnmount(visibleSeconds)
clearInterval(interval)
}
track (nextProps) {
const { isVisible, onVisibilityChange } = this.props
const { visibleSeconds, interval } = this.state
const hasVisibilityChanged = (isVisible !== nextProps.isVisible) || !nextProps
const isVisibleValue = nextProps ? nextProps.isVisible : isVisible
// On visibility change, invoke the callback prop
if (hasVisibilityChanged) {
onVisibilityChange(isVisibleValue)
// If it becomes visible, start counting the `visibleSeconds`
if (isVisibleValue) {
this.setState({
interval: setInterval(() => this.setState({
visibleSeconds: visibleSeconds + 1
}), 1000)
})
} else {
clearInterval(interval)
}
}
}
render () {
return this.props.children
}
}

Passing state values between files

I am having trouble passing the values of state from one file to another. I need to do this because when I get to pulling information from the DB, it will need to display more details about these values.
I have a FlatList that when is clicked it sets the value of the item clicked a state value. But it should also navigate to a new screen, while also passing these values to that new screen to be pulled from the DB.
Here is my function that sets the state and calls the function to pass values to the new screen.
_onSectionListPress = (id) {
this.setState({ jobId: id}, () => this._showJobDetail());
};
Here is my function to navigate to the new screen. But this is where I get stuck on how to pass the values.
_showJobDetail = () => {
this.props.navigation.navigate("JobDescriptions", this.state.stakeholderID, this.state.jobId)
};
I'm using react-navigation to try and do this.
Sorry for asking what seems simple in such a complicated way. I'm still pretty new to react-native and I'm not sure how to do this.
you should pass the state variable like this
this.props.navigation.navigate('JobDescriptions', {
stakeholderID: this.state.stakeholderID,
jobId: this.state.jobId,
});
And call those state variable like this
this.props.navigation.getParam("stakeholderID", "noId")
this.props.navigation.navigate('JobDescriptions', {stakeholderID: this.state.stakeholderID,})
in the new screen you can get value like this
const { navigation } = this.props;
const JobDescriptions= navigation.getParam('JobDescriptions', null);
You should lift the state. One way to do this is to establish a store to keep track of the data. There are many examples of how to do this, including one I described here and off-the-shelf packages like Redux.

Avoid re-rendering a big list of items with react-redux

I am using redux with react and typescript for my application. I am working with many items used at different places of my app. My state looks like this:
{
items: {42: {}, 53: {}, ... }, //A large dictionary of items
itemPage1: {
itemsId: [ 42, 34, 4 ],
...
},
itemPage2: { ...
},
...
}
The user can modify some attributes of the items dispatching some actions. When this happen I need to redraw the components that have been modified in each pages. The issue is that my items are quite big and I cant afford to redraw all of them at each small modification. I was wondering is this approach would work:
I have a fist component <ItemPage1> which connects to the store to get all of the states stored in the tree under itemPage1 e.g. the list of items id: itemsId.
Inside <ItemPage1>, I loop over the itemsId property to generate multiple FilterItem components: itemsId.map( itemId => return <FilterItem id=itemId>);
Finally each Item is connected using ownProps to get the correct part of the state:
const mapStateToItemProps = (state, ownProps) => {
return {
item: state.items[ownProps.id],
}
}
const mapDispatchToItemProps = (dispatch, ownProps) => {
return null;
}
const FilterItem = connect(
mapStateToItemProps,
mapDispatchToItemProps
)(Item)
Can you confirm or refute that if I update the item of id 42, then only this item is going to be re-rendered ?
When rendering big list you need to take into considerations few things :
Lower the total number of DOM elements that you need to render (by not rendering items that are not actually visible on the screen, also known as virtualization)
Don't re-render items that have not changed
Basically, what you want to avoid is a complete re-render of your list (or your page) when the user edits one single row. This can be achieved exactly how you did it, i.e : by passing to the list container only the ids of items that need to be rendered, and to map over these ids to connect each component by using ownProps. If you have a dump <Item/> component, your <ItemPage/> component will create connected connect(<Item/>) component.
This is going to work, if your put a console.log('item rendered') in your <Item/> component class you will notice that there is only one call.
BUT (and it's a big but), what is not obvious when working with react-redux is that all connected components that depends on their ownProps will always rerender if any part of the state change. In your case, even if the <Item/> components will not re-render, their wrapped component connect(Item) will ! If you have few dozens of items, you might encounter some latency if actions need to be dispatched quickly (for example when typing in an input). How to avoid that ? Use a factory function to use ownProps as the initial props :
const mapStateToItemProps = (_, initialProps) => (state) => {
return {
item: state.items[initialProps.id], // we're not relying on the second parameters "ownProps" here, so the wrapper component will not rerender
}
}
const mapDispatchToItemProps = (dispatch, ownProps) => {
return null;
}
const FilterItem = connect(
mapStateToItemProps,
mapDispatchToItemProps
)(Item)
I suggest you to take a look to this other answer.
You might also be interested in these excellent slides : Big List High Performance React & Redux
And finally, you should definitively take a look to react-virtualized to perform the virtualization of your list (i.e, displaying only the item that the user can actually see).
Ok, I've found this discussion: https://github.com/reactjs/redux/issues/1303
At the bottom it is clearly stated (from multiple protagonists):
[...] react-redux takes care of this. It lets you specify specific parts of the state you care about, and takes care to bail out of updating React components when the relevant parts have not changed.
[...] Just wanted to fully understand that what's going on under the hood here, So if the Redux store gets updated but one specific component state hasn't changed, Redux won't trigger the forceUpdate() method for that component? [...]
The wrapper component generated by React-Redux's connect() function does a several checks to try to minimize the number of times your actual component has to re-render. This includes a default implementation of shouldComponentUpdate, and doing shallow equality checks on the props going into your component (including what's returned from mapStateToProps). So yes, as a general rule a connected component will only re-render when the values it's extracting from state have changed.
So I believe my implementation is good, it won't re-render all the items since only one item will see its properties modified.

Resources