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
Related
I decided to add Redux to my pet project (surprise, todolist).
Here's add entry function:
const [todoEntry, setTodoEntry] = useState('');
const addNewEntry = (e) => {
e.preventDefault();
// console.log(todoEntry);
dispatch({
type: ADD_TODO,
payload: {
prodName: todoEntry,
done: false,
favorite: false,
edit: false,
id: uuid()
}
})
setTodoEntry('');
todoEntry comes from another component like that:
<input
id='standartInput'
style={{minWidth: '250px'}}
value={todoEntry}
onChange={e => setTodoEntry(e.target.value)}
type='text'
placeholder='Add new entry (max 55 symbols)' />
Also, I use some hooks to manage my state:
const myTodoItems = useSelector((state) => state.todos[0])
const dispatch = useDispatch()
const [data, setData] = useState(myTodoItems);
And, finally, the reducer:
import { todolist } from "./todolist"
import { ADD_TODO } from '../Store/todoactions'
export const todoReducer = (state = [todolist], action) => {
switch (action.type) {
case ADD_TODO: {
const newItem = action.payload
console.log(newItem)
console.log(todolist)
return ([...todolist, newItem])
}
default:
{ return state }
}
}
The issue is:
todolist exists, I can see at browser console
newItem exists too, I also can see at browser console
BUT! When clicking on 'Add' button, state is not updated.
What I'm doing wrong?
Thanks.
const myTodoItems = useSelector((state) => state.todos[0])
You seem to be selecting only the first item, so it's not surprising that you don't see the rest.
const [data, setData] = useState(myTodoItems);
This looks like an antipattern, why do you need a state variable for something that is already tracked by Redux?
You should also use Redux Toolkit, it is not recommended to use Redux directly.
Edit:
Thank you for the codesandbox, now the problem is clear.
You are using both Redux and React state to deal with the same data, for instance you add todos through Redux but complete them with React state.
A very important principle in React/Redux is to have a single source of truth, but in your case you have two sources of truth for the todos: the Redux store and the useState hook. You use the React state for rendering and initialize it with the Redux state, but then you don't update it when a todo is added, so the UI shows outdated information.
It's fine to use sometimes Redux, sometimes useState as long as it is for independent pieces of data, but for the same data, you need to choose.
Remember that everytime you use useState(initialState) you create a new state variable/source of truth, which will become different from the initial state. Sometimes this is exactly what you want, but not here.
So I would suggest to remove the useState and go through Redux for everything you want to change about the todos (edit them, complete them, and so on).
There are some things you can have as React state (for instance the boolean "are we currently editing this specific todo item"), but then it would be much easier to have a simple useState(false) directly in the TodoItem component.
I've been scratching my head around this one for quite some time now, but I'm still not sure what to do.
Basically, I'm pulling data from a database via useQuery, which all works well and dandy, but I'm also trying to use useReducer (which I'm still not 100% familiar with) to save the initial data as the state so as to detect if any changes have been made.
The problem:
While the useQuery is busy fetching the data, the initial data is undefined; and that's what's being saved as the state. This causes all sorts of problems with regards to validation amd saving, etc.
Here's my main form function:
function UserAccountDataForm({userId}) {
const { query: {data: userData, isLoading: isLoadingUserData} } = useUserData(userId);
const rows = React.useMemo(() => {
if (userData) { /* Process userData here into arrays */ }
return [];
}, [isLoadingUserData, userData]); // Watches for changes in these values
const { isDirty, methods } = useUserDataForm(handleSubmit, userData);
const { submit, /* updateFunctions here */ } = methods;
if (isLoadingUserData) { return <AccordionSkeleton /> } // Tried putting this above useUserDataForm, causes issues
return (
<>
Render stuff here
*isDirty* is used to detect if changes have been made, and enables "Update Button"
</>
)
}
Here's useUserData (responsible for pulling data from the DB):
export function useUserData(user_id, column = "data") {
const query = useQuery({
queryKey: ["user_data", user_id],
queryFn: () => getUserData(user_id, column), // calls async function for getting stuff from DB
staleTime: Infinity,
});
}
return { query }
And here's the reducer:
function userDataFormReducer(state, action) {
switch(action.type) {
case "currency":
return {... state, currency: action.currency}
// returns data in the same format as initial data, with updated currency. Of course if state is undefined, formatting all goes to heck
default:
return;
}
}
function useUserDataForm(handleSubmit, userData) {
const [state, dispatch] = React.useReducer(userDataFormReducer, userData);
console.log(state) // Sometimes this returns the data as passed; most of the times, it's undefined.
const isDirty = JSON.stringify(userData) !== JSON.stringify(state); // Which means this is almost always true.
const updateFunction = (type, value) => { // sample only
dispatch({type: type, value: value});
}
}
export { useUserDataForm };
Compounding the issue is that it doesn't always happen. The main form resides in a <Tab>; if the user switches in and out of the tab, sometimes the state will have the proper initial data in it, and everything works as expected.
The quickest "fix" I can think of is to NOT set the initial data (by not calling the reducer) while useQuery is running. Unfortunately, I'm not sure this is possible. Is there anything else I can try to fix this?
Compounding the issue is that it doesn't always happen. The main form resides in a ; if the user switches in and out of the tab, sometimes the state will have the proper initial data in it, and everything works as expected.
This is likely to be expected because useQuery will give you data back from the cache if it has it. So if you come back to your tab, useQuery will already have data and only do a background refetch. Since the useReducer is initiated when the component mounts, it can get the server data in these scenarios.
There are two ways to fix this:
Split the component that does the query and the one that has the local state (useReducer). Then, you can decide to only mount the component that has useReducer once the query has data already. Note that if you do that, you basically opt out of all the cool background-update features of react-query: Any additional fetches that might yield new data will just not be "copied" over. That is why I suggest that IF you do that, you turn off the query to avoid needless fetches. Simple example:
const App = () => {
const { data } = useQuery(key, fn)
if (!data) return 'Loading...'
return <Form data={data} />
}
const Form = ({ data }) => {
const [state, dispatch] = useReducer(userDataFormReducer, data)
}
since the reducer is only mounted when data is available, you won't have that problem.
Do not copy server state anywhere :) I like this approach a lot better because it keeps server and client state separate and also works very well with useReducer. Here is an example from my blog on how to achieve that:
const reducer = (amount) => (state, action) => {
switch (action) {
case 'increment':
return state + amount
case 'decrement':
return state - amount
}
}
const useCounterState = () => {
const { data } = useQuery(['amount'], fetchAmount)
return React.useReducer(reducer(data ?? 1), 0)
}
function App() {
const [count, dispatch] = useCounterState()
return (
<div>
Count: {count}
<button onClick={() => dispatch('increment')}>Increment</button>
<button onClick={() => dispatch('decrement')}>Decrement</button>
</div>
)
}
If that works is totally dependent on what your reducer is trying to achieve, but it could look like this:
const App = () => {
const { data } = useQuery(key, fn)
const [state, dispatch] = useReducer(userDataFormReducer)
const currency = state.currency ?? data.currency
}
By keeping server state and client state separately, you'll only store what the user has chosen. The "default values" like currency stay out of the state, as it would essentially be state duplication. If the currency is undefined, you can still choose to display the server state thanks to the ?? operator.
Another advantage is that the dirty check is relatively easy (is my client state undefined?) and resets to the initial state also just mean to set the client state back to undefined.
So the actual state is essentially a computed state from what you have from the server and what the user has input, giving precedence to the user input of course.
I need your advice on filtering data with a selector. Suppose I have the following entities in my application. 1 organization has multiple devices which look the following in my state shape:
state {
devices: {
byId: [ 1 { name: device1 } ]
}
organizations: {
byId: [
1 { name: org1, devices: [1,2,3] }
]
}
}
Now I want to filter the devices inside the organization. This is something that I want to do with a selector. My selector looks like the following:
const selectDevice = (id: any) => (state: State) => state.devices.byId[id]
export const selectOrganizationDevices = (id: number) => (state: State) => {
const organization = state.organizations.byId[id] || []
return organization.devices.map(id => selectDevice(id)(state))
}
This should be working fine but my selector got called before I have dispatched the data from redux. I suppose that there's something wrong with my reducer or the component I've created.
My reducer looks like this:
return produce(state, draftState => {
switch (action.type) {
...
case OrganizationActionTypes.FETCH_DEVICES_SUCCESS: {
draftState.byId = action.payload.entities.organizations
draftState.allIds = Object.keys(action.payload.entities.organizations)
draftState.loading = false
break;
}
...
default: {
return state
}
}
})
My functional component looks like the following:
function Devices() {
const dispatch = useDispatch();
const devices = useSelector(selectOrganizationDevices(1))
useEffect(() => {
dispatch(fetchOrganizationDevices(1))
}, [])
const columns = []
return (
<Layout headerName={"Devices"}>
<Table dataSource={devices} columns={columns}/>
</Layout>
)
}
The error I get now is organization.devices is undefined which says that the array of devices in the state is empty. It seems that useSelector is called before dispatch. How can I prevent redux of doing this? Or what should be changed in my code?
Yes, the useEffect hook runs after the first render. useSelector will run during the first render. So, your component code needs to safely handle the case where that data doesn't exist yet.
Also, don't put a hardcoded array/object literal in a selector like that, as it will be a new reference every time and force your component to re-render every time an action is dispatched. Either extract that to a constant outside of the selector, or use a memoization library like Reselect to create the selector.
Finally, you should be using our official Redux Toolkit package, which includes utilities to simplify several common Redux use cases, including store setup, defining reducers, immutable update logic, and even creating entire "slices" of state at once. It also has a new createEntityAdapter API that helps you manage normalized state in the store.
Trying to rewrite this method using react hooks:
this.setState({
isPerformingAuthAction: true
}, () => {
auth.signInWithEmailAndPassword(emailAddress, password).then((value) => {
this.closeSignInDialog(() => {
const user = value.user;
const displayName = user.displayName;
const emailAddress = user.email;
this.openSnackbar(`Signed in as ${displayName || emailAddress}`);
});
}).catch((reason) => {
const code = reason.code;
const message = reason.message;
switch (code) {
case 'auth/invalid-email':
case 'auth/user-disabled':
case 'auth/user-not-found':
case 'auth/wrong-password':
this.openSnackbar(message);
return;
default:
this.openSnackbar(message);
return;
}
}).finally(() => {
this.setState({
isPerformingAuthAction: false
});
});
});
isPerformingAuthAction is a component property that disables buttons while performing various actions.
<Button disabled={state.isPerformingAuthAction} ... />
The Problem: I am using useReducer to manage local state and isPerformingAuthAction is a property of the reducerState. useReducer doesn't return a promise, so I can't do this:
dispatch({type:"isPerformingAuthAction", payload: true}).then(auth.stuff)
I thought about using useState instead, but it doesn't have a call back like this.setState with classes does.
I have been trying to wrap my head around some of the useEffect ways around fetching data and async. I am pretty confused about it, but I think I could have "isPerformingAuthAction" or the state from the reducer as a dependency and have useEffect update the component when it changes but the "isPerformingAuthAction" property changes for other reasons as well, not wtih just auth.signInWithEmailAndPassword(emailAddress, password), but signUp, signOut and a few others.
So when "isPerformingAuthAction" changes to true, it disables buttons so the user is forced to wait for the response from the server. I want to make sure those buttons are disabled (state updated/component re-rendered with greyed-out buttons) before I call the auth.whatever.
[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