Use React for ephemeral state that doesn’t matter to the app globally
and doesn’t mutate in complex ways. For example, a toggle in some UI
element, a form input state. Use Redux for state that matters globally
or is mutated in complex ways. For example, cached users, or a post
draft.
My redux state is only representative of what has been saved to my backend database.
For my use case there is no need for any other part of the application to know about a record in an adding/editing state..
However, all my communication with my API is done through redux-thunks. I have found, getting data from redux into local state for editing is tricky.
The pattern I was trying to use:
const Container = () => {
// use redux thunk to fetch from API
useEffect(() => {
dispatch(fetchThing(id));
}, [dispatch, id]);
// get from redux store
const reduxThing = useSelector(getThing);
const save = thing => {
dispatch(saveThing(thing));
};
return (
{!fetching &&
<ThingForm
defaults={reduxThing}
submit={save}
/>}
);
};
const ThingForm = ({defaults, submit}) => {
const [values, setValues] = useState({ propA: '', propB: '', ...defaults});
const handleChange = { /*standard handleChange code here*/ };
return (
<form onSubmit={() => submit(values)}>
<input type="text" name="propA" value={values.propA} onChange={handleChange} />
<input type="text" name="propB" value={values.propB} onChange={handleChange} />
</form>
);
};
How I understand it, ThingForm is unmounted/mounted based upon "fetching." However, it is a race condition whether or not the defaults get populated. Sometimes they do, sometimes they don't.
So obviously this isn't a great pattern.
Is there an established pattern for moving data into local state from redux for editing in a form?
Or should I just put my form data into redux? (I don't know if this would be any easier).
EDIT: I think this is essentially what I am fighting: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
But no recommendation really clearly fits. I am strictly using hooks. I could overwrite with useEffect on prop change, but seems kind of messy.
EDIT:
const Container = () => {
// use redux thunk to fetch from API
useEffect(() => {
dispatch(fetchThing(id));
}, [dispatch, id]);
// get from redux store
const reduxThing = useSelector(getThing);
const save = thing => {
dispatch(saveThing(thing));
};
return (
{!fetching &&
<ThingForm
defaults={reduxThing}
submit={save}
/>}
);
};
const ThingForm = ({defaults, submit}) => {
const [values, setValues] = useState({ propA: '', propB: '', ...defaults});
const handleChange = { /*standard handleChange code here*/ };
useEffect(() => {
setValues({...values, ...defaults})
}, [defaults]);
const submitValues = (e) => {
e.preventDefault();
submit(values)
}
return (
<form onSubmit={submitValues}>
<input type="text" name="propA" value={values.propA} onChange={handleChange} />
<input type="text" name="propB" value={values.propB} onChange={handleChange} />
</form>
);
};
What you are doing is the right way, there's no reason why you should put the form data in the redux store. Like you said, "there is no need for any other part of the application to know about a record in an adding/editing state"
And that's correct.
The only problem you have is here:
{!fetching &&
<ThingForm
defaults={reduxThing}
submit={save}
/>}
Assuming fetching is true on every dispatch:
Instead of trying to hide the component (unmounting essentially), you should maybe use a spinner that overlays the page?
I don't know the rest of your code to comment on a better approach.
You also don't have to add dispatch to the dependency array
useEffect(() => {
dispatch(fetchThing(id));
}, [id]);
From the react docs:
React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
Related
I have an array of Notes that I get from my database, the notes objects each have a category assigned to it. There are also buttons that allow the user to filter the notes by category and only render the ones with the corresponding one.
Now, it's all working pretty well but there's one annoying thing that I can't get rid of: whenever I click on any of the buttons: <button onClick={() => {handleClick(categoryItem.category)}}>{categoryItem.category}</button>, the filterNotes() function is only called on the second click. I suspect it has to do something with me calling setState() twice, or maybe with the boolean that I set in the functions, but I tried various combinations to call the function on the first click, but to no avail so far.
Here's my MainArea code:
import React, { useState, useEffect } from "react";
import Header from "./Header";
import Footer from "./Footer";
import ListCategories from "./ListCategories";
import Note from "./Note";
import axios from "axios"
function CreateArea(props) {
const [isExpanded, setExpanded] = useState(false);
const [categories, setCategories] = useState([])
const [notes, setNotes] = useState([])
const [fetchB, setFetch] = useState(true)
const [filterOn, setFilter] = useState(false)
const [note, setNote] = useState({
title: "",
content: "",
category: ''
});
useEffect(() => {
fetch('http://localhost:5000/categories')
.then(res => res.json())
.then(json => setCategories(json))
}, [])
useEffect(() => {
if(fetchB) {
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
console.log(json)
setNotes(json)
setFetch(false)
})
}
}, [fetchB])
function handleChange(event) {
const { name, value } = event.target;
console.log("handleChange called")
setNote(prevNote => {
return {
...prevNote,
[name]: value
};
});
}
function submitNote(e){
e.preventDefault();
axios.post("http://localhost:5000/notes/add-note", note)
.then((res) => {
setNote({
category: '',
title: "",
content: ""
})
setFetch(true)
console.log("Note added successfully");
console.log(note)
})
.catch((err) => {
console.log("Error couldn't create Note");
console.log(err.message);
});
}
function expand() {
setExpanded(true);
}
function filterNotes(category){
if(filterOn){
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
console.log("filter notes")
setNotes(json)
setNotes(prevNotes => {
console.log("setNotes called with category " + category)
return prevNotes.filter((noteItem) => {
return noteItem.category === category;
});
});
setFilter(false)
})
}
}
return (
<div>
<Header/>
<ListCategories categories={categories} notes={notes} filterNotes={filterNotes} setFilter={setFilter} filterOn={filterOn} setFetch={setFetch}/>
<form className="create-note">
{isExpanded && (
<input
name="title"
onChange={handleChange}
value={note.title}
placeholder="Title"
/>
)}
<textarea
name="content"
onClick={expand}
onChange={handleChange}
value={note.content}
placeholder="Take a note..."
rows={isExpanded ? 3 : 1}
/>
<select
name="category"
onChange={handleChange}
value={note.category}>
{
categories.map(function(cat) {
return <option
key={cat.category} value={cat.value} > {cat.category} </option>;
})
}
</select>
<button onClick={submitNote}>Add</button>
</form>
<Note notes={notes} setFetch={setFetch}/>
<Footer/>
<button onClick={()=>{setFetch(true)}}>All</button>
</div>
);
}
export default CreateArea;
And ListCategories where I get call the function and get the chosen category from the buttons:
import React, { useState } from "react";
import CreateCategory from "./CreateCategory";
export default function ListCategories(props) {
function handleClick(category){
props.setFilter(true)
props.filterNotes(category)
}
return (
<div className="category-group">
<CreateCategory/>
<div className="btn-group">
{props.categories.map((categoryItem, index) =>{
return(
<button onClick={() => {handleClick(categoryItem.category)}}>{categoryItem.category}</button>
)
})}
</div>
</div>
)
}
I'm not sure what the best practice is with such behaviour - do I get the notes from the database each time as I'm doing now or should I do something completely different to avoid the double-click function call?
Thanks in advance for any suggestions!
Your issue is this function:
function handleClick(category){
props.setFilter(true)
props.filterNotes(category)
}
Understand that in React, state is only updated after the current execution context is finished. So in handleClick() when you call setFiler(), that linked filterOn state is only updated when the rest of the function body finishes.
so when your filterNotes() function is called, when it evaluates filterOn, it is still false, as it was initially set. After this function has executed, the handleClick() function has also finished, and after this, the filterOn state now equals true
This is why on the second click, the desired rendering effect occurs.
There are multiple ways to get around this, but I normally use 'render/don't-render' state by including it as an embedded expression in the JSX:
<main>
{state && <Component />}
</main>
I hope this helps.
You diagnosed the problem correctly. You shouldn't be using state like you would a variable. State is set asynchronously. So, if you need to fetch some data and filter it, do that and THEN add the data to state.
function filterNotes(category){
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
const filtered = json.filter((noteItem) => (noteItem.category === category));
setNotes(filtered);
})
}
}
It's not clear to me why you would need the filterOn state at all.
Depending on how your frequently your data is updated and if you plan on sharing data across users, the answer to this question will vary.
If these notes are specific to the user then you should pull the notes on load and then store them in a local state or store. Write actions that can update the state or store so that this isn't coupled with your react UI rendering. Example: https://redux.js.org/ or https://mobx.js.org/README.html.
Then update that store and your remote database accordingly through dispatching actions. This avoids lots of calls to the database and you can perform your filtering client-side as well. You can then also store data locally for offline use through this method so if it's for a mobile app and they lose internet connection, it'll still render. Access the store's state and update your UI based on that. Specifically the notes and categories.
If you have multiple users accessing the data then you'll need to look at using websockets to send that data across clients in addition to the database. You can add listeners that look for this data and update that store or state that you will have created previously.
There are many approaches to this, this is just an approach I would take.
You could also create a context and provider that maintains your state on the first load and persists after that. Then you can avoid passing down state handlers through props
Hello i'm trying to create some kind of lottery and i'm wondering which approach of modifying state by actions payload should be used.
Let's say i have state
type initialCartState = {
productsFromPreviousSession: Product[]
selectedProduct: Product
balance: number,
productsInCart: Product[]
}
and our reducer looks like
const reducers = {
addProduct(state, action) => {
state.products.push(state.action.payload.product)
},
addProductsFromPreviousSession(state, action) => {
state.products.push(...state.productsFromPreviousSession)
},
}
And i noticed i used completely two different approaches with these two types cuz in my component it looks like
const component = () => {
const selectedProduct = useSelector(state => state.cart.selectedProduct);
const availableBalance = useSelector(state => state.cart.balance - sum(state.cart.products, 'price'));
const dispatch = useDispatch()
const sumOfProductsFromPreviousSession = useSelector(state => sum(state.cart.products,'price'))
return (
<div>
<div onClick={() => {
if((balance - selectedProduct.price) > 0) {
dispatch(cartActions.addProduct(selectedProduct))
}
}}/>
<div onClick={() => {
if((balance - sumOfProductsFromPreviousSession) > 0) {
dispatch(cartActions. addProductsFromPreviousSession())
}
}}/>
</div>
)
}
There are two different types of handling actions, in addProduct i used selector and pass value in action payload. In Add products from previous session we rely on state inside reducer (Also have middleware for purpose of saving in localStorage, but there i used store.getState()). Which kind of approach is correct ?
Also how it will change when we move balance to another reducer, and then we will not have access to that i cartReducer?
I saw there are bunch of examples on counter when increment and decrement rely on current reducerState and there are actions without payload, but there is no validation which is used in my example.
Thanks in advance !
Both approaches can be used. Basically, if you need to show state data in UI or other parts of processes, you should read with selector. This way, changes inside the store can be reflected in the components reactively.
In your case, you are just updating the state value with currently available data from the state. So, you can dispatch action without payload.
In your example, even though you pass the payload from onClick event, you are still reading the value from the state itself.
I am pulling in data from an API, and then allowing a user to modify that data within react, after which I will send the updated state to the server.
My API provides the following JSON:
{
"id": 1,
"title": "Test Campaign",
"impressions": 12,
}
And my component looks like so:
function Campaign() {
const [data, setData] = useState([])
useEffect(async () => {
const result = await axios('/campaigns/1')
setData(result.data);
}, []);
const handleChange = (event) => {
setData({[event.target.name]: event.target.value});
}
return (
<div className='row'>
<div className='col-12'>
<h2>{ data.title }</h2>
<input
type='number'
value={data.impressions}
name='impressions'
onChange={e => handleChange(e)}
/>
</div>
</div>
)
}
However, running this code causes all keys in the data state to be overwritten (and thus my h2 tag displays nothing). It was my understanding that React State carried out a shallow merge, and so in this case would only update the impressions key:value?
Functional component's useState hook doesn't actually shallow merge state updates so you need to manage this yourself.
Use a functional state update to access the previous state to shallow copy into the next state.
Note
Unlike the setState method found in class components, useState
does not automatically merge update objects. You can replicate this
behavior by combining the function updater form with object spread
syntax:
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
You may want to also ensure you maintain your state invariant. Since you are updating your state via key-value pairs I'm assuming you are actually using an object.
const [data, setData] = useState({}); // <-- empty object initial state
const handleChange = (event) => {
setData(data => ({
...data, // <-- shallow copy existing state
[event.target.name]: event.target.value,
}));
}
I know this question is already asked here: How to set initial state for useState Hook in jest and enzyme?
const [state, setState] = useState([]);
And I totally agree with Jimmy's Answer to mock the useState function from test file but I have some extended version of this question, "What if I have multiple useState statements into the hooks, How can I test them and assign the respective custom values to them?"
I have some JSX rendering with the condition of hook's state values and depending on the values of that state the JSX is rendering.
How Can I test those JSX by getting them into the wrapper of my test case code?
Upon the answer you linked, you can return different values for each call of a mock function:
let myMock = jest.fn();
myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce('x')
.mockReturnValue(true);
In my opinion this is still brittle. You may modify the component and add another state later, and you would get confusing results.
Another way to test a React component is to test it like a user would by clicking things and setting values on inputs. This would fire the event handlers of the component, and React would update the state just as in real configuration. You may not be able to do shallow rendering though, or you may need to mock the child components.
If you prefer shallow rendering, maybe read initial state values from props like this:
function FooComponent({initialStateValue}) {
const [state, setState] = useState(initialStateValue ?? []);
}
If you don't really need to test state but the effects state has on children, you should (as some comments mention) just then test the side effect and treat the state as an opaque implementation detail. This will work if your are not using shallow rendering or not doing some kind of async initialization like fetching data in an effect or something otherwise it could require more work.
If you cant do the above, you might consider removing state completely out of the component and make it completely functional. That way any state you need, you can just inject it into the component. You could wrap all of the hooks into a 'controller' type hook that encapsulates the behavior of a component or domain. Here is an example of how you might do that with a <Todos />. This code is 0% tested, its just written to show a concept.
const useTodos = (state = {}) => {
const [todos, setTodos] = useState(state.todos);
const id = useRef(Date.now());
const addTodo = useCallback((task) => {
setTodos((current) => [...current, { id: id.current++, completed: false, task }]);
}, []);
const removeTodo = useCallback((id) => {
setTodos((current) => current.filter((t) => t.id !== id));
}, []);
const completeTodo = useCallback((id) => {
setTodos((current) => current.map((t) => {
let next = t;
if (t.id === id) {
next = { ...t, completed: true };
}
return next;
}))
}, []);
return { todos, addTodo, removeTodo, completeTodo };
};
const Todos = (props) => {
const { todos, onAdd, onRemove, onComplete } = props;
const onSubmit = (e) => {
e.preventDefault();
onAdd({ task: e.currentTarget.elements['todo'].value });
}
return (
<div classname="todos">
<ul className="todos-list">
{todos.map((todo) => (
<Todo key={todo.id} onRemove={onRemove} onComplete={onComplete} />
)}
</ul>
<form className="todos-form" onSubmit={onSubmit}>
<input name="todo" />
<button>Add</button>
</form>
</div>
);
};
So now the parent component injects <Todos /> with todos and callbacks. This is useful for testing, SSR, ect. Your component can just take a list of todos and some handlers, and more importantly you can trivially test both. For <Todos /> you can pass mocks and known state and for useTodos you would just call the methods and make sure the state reflects what is expected.
You might be thinking This moves the problem up a level and it does, but you can now test the component/logic and increase test coverage. The glue layer would require minimal if any testing around this component, unless you really wanted to make sure props are passed into the component.
So taking a look at the Apollo useMutation example in the docs https://www.apollographql.com/docs/react/data/mutations/#tracking-loading-and-error-states
function Todos() {
...
const [
updateTodo,
{ loading: mutationLoading, error: mutationError },
] = useMutation(UPDATE_TODO);
...
return data.todos.map(({ id, type }) => {
let input;
return (
<div key={id}>
<p>{type}</p>
<form
onSubmit={e => {
e.preventDefault();
updateTodo({ variables: { id, type: input.value } });
input.value = '';
}}
>
<input
ref={node => {
input = node;
}}
/>
<button type="submit">Update Todo</button>
</form>
{mutationLoading && <p>Loading...</p>}
{mutationError && <p>Error :( Please try again</p>}
</div>
);
});
}
This seems to have a major flaw (imo), updating any of the todos will show the loading state for every single todo, not just the one that has the pending mutation.
And this seems to stem from a larger problem: there's no way to track the state of multiple calls to the same mutation. So even if I did want to only show the loading state for the todos that were actually loading, there's no way to do that since we only have the concept of "is loading" not "is loading for todo X".
Besides manually tracking loading state outside of Apollo, the only decent solution I can see is splitting out a separate component, use that to render each Todo instead of having that code directly in the Todos component, and having those components each initialize their own mutation. I'm not sure if I think that's a good or bad design, but in either case it doesn't feel like I should have to change the structure of my components to accomplish this.
And this also extends to error handling. What if I update one todo, and then update another while the first update is in progress. If the first call errors, will that be visible at all in the data returned from useMutation? What about the second call?
Is there a native Apollo way to fix this? And if not, are there options for handling this that may be better than the ones I've mentioned?
Code Sandbox: https://codesandbox.io/s/v3mn68xxvy
Admittedly, the example in the docs should be rewritten to be much clearer. There's a number of other issues with it too.
The useQuery and useMutation hooks are only designed for tracking the loading, error and result state of a single operation at a time. The operation's variables might change, it might be refetched or appended onto using fetchMore, but ultimately, you're still just dealing with that one operation. You can't use a single hook to keep track of separate states of multiple operations. To do that, you need multiple hooks.
In the case of a form like this, if the input fields are known ahead of time, then you can just split the hook out into multiple ones within the same component:
const [updateA, { loading: loadingA, error: errorA }] = useMutation(YOUR_MUTATION)
const [updateB, { loading: loadingB, error: errorB }] = useMutation(YOUR_MUTATION)
const [updateC, { loading: loadingC, error: errorC }] = useMutation(YOUR_MUTATION)
If you're dealing with a variable number of fields, then we have to break out this logic into a separate because we can't declare hooks inside a loop. This is less of a limitation of the Apollo API and simply a side-effect of the magic behind hooks themselves.
const ToDo = ({ id, type }) => {
const [value, setValue] = useState('')
const options = { variables = { id, type: value } }
const const [updateTodo, { loading, error }] = useMutation(UPDATE_TODO, options)
const handleChange = event => setValue(event.target.value)
return (
<div>
<p>{type}</p>
<form onSubmit={updateTodo}>
<input
value={value}
onChange={handleChange}
/>
<button type="submit">Update Todo</button>
</form>
</div>
)
}
// back in our original component...
return data.todos.map(({ id, type }) => (
<Todo key={id} id={id} type={type] />
))