Programmatically observing changes in Recoil with React - reactjs

I'm using Recoil (recoiljs.org) in my react project.
I have a fairly classic setup with a list of items - the object graph looks something like this:
Routes - routes is an array of Route objects
- Route - route object is edited in a form
I use an atom to represent the currently selected Routes array, e.g.
const [routes, setRoutes] = useRecoilValue(currentRoutesViewState);
And I also use a selectorFamily to allow individual controls to bind to and update the state of an individual item.
const routeSelectorFamily = selectorFamily({
key: "routeSelectorFamily",
get:
(routeKey) =>
({ get }) => {
const routes = get(currentRoutesViewState);
return routes.find((r) => r.key === routeKey);
},
set:
(routeKey) =>
({ set }, newValue) => {
set(currentRoutesViewState, (oldRoutes) => {
const index = oldRoutes.findIndex((r) => r.key === routeKey);
const newRoutes = replaceItemAtIndex(
oldRoutes,
index,
newValue as RouteViewModel
);
return newRoutes;
});
},
});
As you can see, the set function allows someone to update an individual route state and that is replicated up into anything rendering routes.
However, I would like to auto-save any changes to this whole graph but don't know how to easily observe any changes to the Routes parent object. Is there a way to programmatically subscribe to updates so I can serialize and save this state?

You can just use a useEffect for that:
const currentState = useRecoilValue(currentRoutesViewState);
useEffect(() => {
// currentState changed.
// save the current state
}, [currentState])

Related

React Hook not notifying state change to components using the hook

So I have a Hook
export default function useCustomHook() {
const initFrom = localStorage.getItem("startDate") === null? moment().subtract(14, "d"): moment(localStorage.getItem("startDate"));
const initTo = localStorage.getItem("endDate") === null? moment().subtract(1, "d"): moment(localStorage.getItem("endDate"));
const [dates, updateDates] = React.useState({
from: initFrom,
to: initTo
});
const [sessionBreakdown, updateSessionBreakdown] = React.useState(null);
React.useEffect(() => {
api.GET(`/analytics/session-breakdown/${api.getWebsiteGUID()}/${dates.from.format("YYYY-MM-DD")}:${dates.to.format("YYYY-MM-DD")}/0/all/1`).then(res => {
updateSessionBreakdown(res.item);
console.log("Updated session breakdown", res);
})
},[dates])
const setDateRange = React.useCallback((startDate, endDate) => {
const e = moment(endDate);
const s = moment(startDate);
localStorage.setItem("endDate", e._d);
localStorage.setItem("startDate", s._d);
updateDates((prevState) => ({ ...prevState, to:e, from:s}));
}, [])
const getDateRange = () => {
return [dates.from, dates.to];
}
return [sessionBreakdown, getDateRange, setDateRange]
}
Now, this hook appears to be working in the network inspector, if I call the setDateRanger function I can see it makes the call to our API Service, and get the results back.
However, we have several components that are using the sessionBreakdown return result and are not updating when the updateSessionBreakdown is being used.
i can also see the promise from the API call is being fired in the console.
I have created a small version that reproduces the issue I'm having with it at https://codesandbox.io/s/prod-microservice-kq9cck Please note i have changed the code in here so it's not reliant on my API Connector to show the problem,
To update object for useState, recommended way is to use callback and spread operator.
updateDates((prevState) => ({ ...prevState, to:e, from:s}));
Additionally, please use useCallback if you want to use setDateRange function in any other components.
const setDateRange = useCallback((startDate, endDate) => {
const e = moment(endDate);
const s = moment(startDate);
localStorage.setItem("endDate", e._d);
localStorage.setItem("startDate", s._d);
updateDates((prevState) => ({ ...prevState, to:e, from:s}));
}, [])
Found the problem:
You are calling CustomHook in 2 components separately, it means your local state instance created separately for those components. So Even though you update state in one component, it does not effect to another component.
To solve problem, call your hook in parent component and pass the states to Display components as props.
Here is the codesandbox. You need to use this way to update in one child components and use in another one.
If wont's props drilling, use Global state solution.

Where to calculate groups for fluentui detaillist in nextjs

I am learning react, and given this simple example of using SWR to fetch some items from an API and showing the items with groups using fluentui DetailedList - I am running into a problem with the groups.
Whenever I click a group in UI to collapse/uncollapse, that seems to trigger a rerender, and then the component will createGroups(data) again which resets the UI again back to original state as the groups object is recalculated.
Where am I supposed to actually store / calculate the groups information of my data? Initial it needs to be created, but from there it seems that it should only needs to be reevaluated whenvere the swr api returns new data - and then i still properly would want to merge in the current state from collapsed groups that the user might have changed in the UI.
Is it because i properly should not use SWR as it refreshes data live - and only do it on page refresh?
const SWR = ({ children, listid, onSuccess }: { children: ((args: SWRResponse<any, any>) => any), listid: string, onSuccess?: any }) => {
const url = `http://localhost:7071/api/Lists/${listid}`;
console.log(url);
const {data,error } = useSWR(url, { fetcher: fetcher, isPaused: () => listid === undefined, onSuccess });
const items = data.value;
const groups = createGroups(data)
return <... DetailsList group={groups} items={items} ... >; // ... left out a few details ...
};
What about adding a state for holding the groups and an useEffect for when data changes and insde the useEffect you should check if the content has changed before updating the groupState.
const hasChanged(data) => {
return data.notEquals(state.data)); // write your own logic for comparing the result
};
useEffect(() => { if (hasChanged(data)) {
setState(prev=> ({ ...prev, group: createGroup(data), data: data });
}}, [data]);
You dont actually need to store the group, you can just hold the data in your state, but the important part is to be able to check if any change actually took place before changing the state.
Another thing worth trying is the compare option in the useSWR hook. So instead of placing the "hasChanged" logic inside an useEffect hook, perhaps it could be in the compare function. Haven't had the chanse to test this myself though.
A third and final option would be to place the creation of groups inside your fetcher. Perhaps the most intuitive solution for this particular case, though I'm not completely sure it will prevent the unnecessary re-renders.
const fetcher = url => axios.get(url).then(res=> {
return {
items: res.data.value,
groups: createGroups(res.data),
};
});
const SWR = ({ children, listid, onSuccess }: { children: ((args: SWRResponse<any, any>) => any), listid: string, onSuccess?: any }) => {
const { data, error } = useSWR(url, fetcher, ...);
return <... DetailsList group={data.groups} items={data.items} ... >; // ... left out a few details ...
};

Special actions on React state variables

I have a React component with a state variable that needs specific actions. For example, consider a component that shows a list of user profiles, and the user can switch to another profile or create a new one. The state variable is a list of user profiles, and a second variable is the currently selected profile; the component can add a new profile (which is more specific than just "setting" a new list of profiles), or it can change the currently selected profile.
My first idea was to have two useState hooks, one for the list and one for the current profile. However, one problem with that is that I would like to store the current profile's id, which refers to one of the profiles in the list, which means that the two state variables are inter-dependent. Another issue is that having a generic setProfiles state change function is a bit too "open" for my taste: the add logic may be very specific and I would like to encapsulate it.
So I came up with this solution: a custom hook managing the two state variables and their setters, that would expose the two values (list and current id) and their appropriate actions (add new profile and select profile).
This is the code of the hook:
export const useProfileData = () => {
const [ profiles, setProfiles ] = useState([]);
const [ currentProfileID, setCurrentProfileID ] = useState(null);
const [ currentProfile, setCurrentProfile ] = useState(null);
useEffect(() => {
// This is actually a lazy deferred data fetch, but I'm simplifying for the sake of brevity
setProfiles(DataManager.getProfiles() || [])
}, [])
useEffect(() => {
if (!profiles) {
setCurrentProfile(null);
return;
}
const cp = profiles.find(p => p.ID === currentProfileID);
setCurrentProfile(cp);
}, [ currentProfileID, profiles ])
return {
currentProfile: currentProfile,
profiles: profiles,
setCurrentProfileID: i_id => setCurrentProfileID(i_id),
addNewProfile: i_profile => {
profiles.push(i_profile);
setProfiles(profiles);
DataManager.addNewProfile(i_profile); // this could be fire-and-forget
},
};
};
Three states are used: the list, the current profile id and the current profile (as an object). The list is retrieved at mounting (the current id should be too, but I omitted that for brevity). The current profile is never set directly from the outside: the only way to change it is to change the id or the list, which is managed by the second useEffect. And the only way to change the id is through the exposed setCurrentProfileID function.
Adding a new profile is managed by an exposed addNewProfile function, that should add the new profile to the list in state, update the list in state, and add the new profile in the persistent DataManager.
My first question is: is it ok to design a hook like this? From a general software design point of view, this code gives encapsulation, separation of concerns, and a correct state management. What I'm not sure about if this is proper in a functional world like React.
My second question is: why is my component (that uses useProfileData) not updated when addNewProfile is called? For example:
const ProfileSelector = (props) => {
const [ newProfileName, setNewProfileName ] = useState('');
const { profiles, currentProfile, setCurrentProfileID, addNewProfile } = useProfileData();
function createNewProfile() {
addNewProfile({
name: newProfileName,
});
}
return (
<div>
<ProfilesList profiles={profiles} onProfileClick={pid => setCurrentProfileID(pid)} />
<div>
<input type="text" value={newProfileName} onChange={e => setNewProfileName(e.target.value)} />
<Button label="New profile" onPress={() => createNewProfile()} />
</div>
</div>
);
};
ProfilesList and Button are components defined elsewhere.
When I click on the Button, a new profile is added to the persistent DataManager, but profiles is not updated, and ProfilesList isn't either (of course).
I'm either implementing something wrong, or this is not a paradigm that can work in React. What can I do?
EDIT
As suggested by #thedude, I tried using a reducer. Here is the (stub) of my reducer:
const ProfilesReducer = (state, action) => {
const newState = state;
switch (action.type) {
case 'addNewProfile':
{
const newProfile = action.newProfile;
newState.profiles.push(newProfile);
DataManager.addNewProfile(newProfile);
}
break;
default:
throw new Error('Unexpected action type: ' + action.type);
}
return newState;
}
After I invoke it (profilesDispatch({ type: 'addNewProfile', newProfile: { name: 'Test' } });), no change in profilesState.profiles is detected - or at least, a render is never triggered, nor an effect. However, the call to DataManager has done its job and the new profile has been persisted.
You should never mutate your state, not even in a reducer function.
From the docs:
If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
Change your reducer to return a new object:
const ProfilesReducer = (state, action) => {
switch (action.type) {
case 'addNewProfile':
{
const newProfile = action.newProfile;
return {...state, profiles: [...state.profiles, newProfile]}
}
break;
default:
throw new Error('Unexpected action type: ' + action.type);
}
return state;
}
Also not that reducer should no have side effects, if you want to perform some action based on a state change, use a useEffect hook for that.
For example:
DataManager.addNewProfile(newProfile) should not be called from the reducer

Component not updating on deeply nested redux object

I have a project portion of my app, and users can create events within the project. My redux state is deeply nested, which looks like:
When users create an event, I update state with the following:
case CREATE_PROJECT_TODO:
const data = [...state.data];
const index = state.data.findIndex(project => project._id===action.payload.project_id);
data[index].events = [
...data[index].events,
action.payload
];
return {
...state,
data
};
However, my react component isn't updating to reflect the changes. I'm quite sure I'm not mutating state. Is it an issue with deeply nested objects, and react can't detect those changes! Any help would be appreciated!
With const data = [...state.data], you are doing a shallow copy.
Use map and update your state. Your state is updated correctly and will trigger the component re-render properly.
case CREATE_PROJECT_TODO:
const index = state.data.findIndex((project) => project._id === action.payload.project_id)
const updatedData = state.data.map((item, idx) => {
if (idx === index) {
return {
...item,
events: [...item.events, action.payload],
}
}
return item
})

React-Redux connect: Use mapStatetoProps to inject only component part of store

[React-Redux] Issue.
I'd like to have reusable encapsulated components to be used in any app, or in any level of the app's store.
When it comes to use 'mapStatetoProps' then making the component container (injecting the state into the component as props), you always receive the whole store. This might be a pain if you want to reuse components dynamically or in other projects.
The thing is if you use the same store entry but you want to use the same component as encapsulated module they will be sharing the same data.
And also, when you are encapsulating components and you reuse them and they are deep nested in the store, you will end up needing to know where they are.
A possible ugly solution would be to implement a script going through the state inside the mapStateToProps till it finds the key matching certain name. The issue here would be to make sure the state field you want to use is unique.
I'll be more than happy to know any proper solution to this problem in an elegant way.
Or maybe we are just thinking the wrong way when talking about react-redux-sagas apps.
For the sake of the example, I'll be talking about a reusable editor component, that edits documents and submits them to server.
In the code that is using the editor, I give each editor a unique id. E.g.
const Comment = (props) => {
return <div>
<h3>Add new comment</h3>
<Editor editorId={`new-comment-${props.commentId}`} />
</div>
}
In the redux state, I have one subreducer editor with objects keyed by the editorId, so the redux state is something like:
{
otherStuff: {...},
editor: {
"new-comment-123": { isActive: true, text: "Hello there" },
"new-comment-456": { isActive: false, text: "" },
...
},
...
}
Then in Editor mapStateToProps I use selectors to get the data for the correct instance:
const mapStateToProps = (state, ownProps) => {
return {
isActive: selectors.isActive(state, ownProps.editorId),
text: selectors.text(state, ownProps.editorId)
}
}
The selectors are built in reselect style, either manually or by actually using reselect. Example:
// Manual code
export const getEditor = (state, editorId) => state.editor[editorId] || {};
export const isActive = (state, editorId) => getEditor(state, editorId).
export const text = (state, editorId) => getEditor(state, editorId).text;
// Same in reselect
import { createSelector } from 'reselect'
export const getEditor = (state, editorId) => state.editor[editorId] || {};
export const isActive = createSelector([getEditor], (editorData) => editorData.isActive);
export const text = createSelector([getEditor], (editorData) => editorData.text);
If you want to extend this to be used in multiple apps, you need to export your component, reducer and sagas. For a working example, check out https://github.com/woltapp/redux-autoloader or even http://redux-form.com
If I understand your concern correctly, you could implement mapStateToProps as if it receives the part of state you need and call it, say, mapStateToYourComponentProps, and in actual mapStateToProps you just call mapStateToYourComponentProps and pass it appropriate part of state
I found a way to make the components totally independent from state and hierarchy within the app.
Basically, each component must expose a method to set the path within the state. Then you have to initialize it when either when you import it before using it. You could also implement it in another way so you receive it inline as a prop.
It makes uses of reselect to establish the selection.
Each component knows the name of its key in the state.
The root component will import other components and it will call the setPath method of each one passing the root component path.
Then each component will call the setPath of each subcomponent passing their own location in the state. "Each parent will init their children"
So each component will set a path in the store based on the naming "parent path + local path (component key name in the store)".
This way you would be defining a nested routing with 'createSelector' method from reselect, like this: ['rootKey','subComponent1Key','subsubComponent1Key].
With this, you have the store isolation completed. Redux actions will just change the bit needed so yo have this part also covered by the framework.
It worked like a charm for me, please let me know if its good before I mark it as good.
If you have some free time, try the npm package redux-livequery (https://www.npmjs.com/package/redux-livequery) I just wrote recently.
There is another way to manage your active list.
let selector0 = (state) => state.task.isComplete;
let selector1 = (state) => state.task.taskList;
this.unsub2 = rxQueryBasedOnObjectKeys([selector0, selector1], ['isActive', 'task'], (completeTaskList) => {
// equal SQL =>
// select * from isActive LEFT JOIN taskList on isActive.child_key == taskList.child_key
console.log("got latest completeTaskList", completeTaskList);
// you can do whatever you want here
// ex: filter, reduce, map
this.setState({ completeTaskList });
}, 0);
In the reducer:
case "MARK_ACTIVE_TASK": {
let { id } = action.meta;
return update(state, { isActive: { [id]: { $set: { active: Date.now() } } } });
}
case "UNMARK_ACTIVE_TASK": {
let { id } = action.meta;
return update(state, { isActive: { $apply: function (x) { let y = Object.assign({}, x); delete y[id]; return y; } } });
}
It lets you have simpler reducer. In addition, there is no more nested selector function or filter which is really expensive operation. Putting your all logic in the same place would be great.
And it can do even more complexity operation like how to get complete and active list.
let selector0 = (state) => state.task.isComplete;
let selector1 = (state) => state.task.isActive;
let selector2 = (state) => state.task.taskList;
this.unsub3 = rxQueryInnerJoin([selector0, selector1, selector2], ['isComplete', 'isActive', 'task'], (completeAndActiveTaskList) => {
// equal SQL =>
// select * from isComplete INNER JOIN isActive on isComplete.child_key == isActive.child_key
// INNER JOIN taskList on isActive.child_key == taskList.child_key
console.log("got latest completeAndActiveTaskList", completeAndActiveTaskList);
// you can do whatever you want here
// ex: filter, reduce, map
this.setState({ completeAndActiveTaskList });
}, 0);
If you would like to get complete or active list, it's also easy to get.
The more example, please refer to the sample code => https://github.com/jeffnian88/redux-livequery-todos-example

Resources