A bit new to React here.
While developing a personal project based on React, I often came up against scenarios where I needed child components to update state passed down to it by a parent, and have the updated state available in both child and parent components.
I know React preaches a top-down flow of immutable data only, but is there an elegant way of solving this issue?
The way I came up with is as shown below. It gets the job done, but it looks and feels ugly, bloated, and it makes me think I'm missing something obvious here that a well-established framework like React would've definitely accounted for in a more intuitive way.
As a simple example, assume that I have a couple of nested components:
Root
Author
Post
Comment
And that I need each child component to be able to modify the state such that it is also accessible to its parent component as well. A use case might be that you could interact with the Comment component to edit a comment, and then interact with a SAVE button defined in the Root component to save the entire state to a database or something like that.
The way I presently handle such scenarios is this:
const Root = ({}) => {
const [data, setData] = React.useState({
author: {
name: 'AUTHOR',
post: {
content: 'POST',
comment: {
message: 'COMMENT'
}
}
}
});
const onEditAuthor = value => {
setData({ author: value });
}
const onSave = () => {
axios.post('URL', data);
}
return <>
<Author author={data.author} onEditAuthor={onEditAuthor} />
<button onClick={() => onSave()}>SAVE</button>
</>
}
const Author = ({ author, onEditAuthor }) => {
const onEditPost = value => {
onEditAuthor({ name: author.name, post: value });
}
return <Post post={author.post} onEditPost={onEditPost} />
}
const Post = ({ post, onEditPost }) => {
const onEditComment = value => {
onEditPost({ content: post.content, comment: value });
}
return <Comment comment={post.comment} onEditComment={onEditComment} />
}
const Comment = ({ comment, onEditComment }) => {
return <input defaultValue={comment.message} onChange={ev => onEditComment({ message: ev.target.value })} />
}
When you change the Comment, it calls the Post.onEditComment() handler, which in turn calls the Author.onEditPost() handler, which finally calls the Root.onEditAuthor() handler. This finally updates the state, causing a re-render and propagates the updated state all the way back down.
It gets the job done. But it is ugly, bloated, and looks very wrong in the sense that the Post component has an unrelated onEditComment() method, the Author component has an unrelated onEditPost() method, and the Root component has an unrelated onEditAuthor() method.
Is there a better way to solve this?
Additionally, when the state finally changes, all components that rely on this state are re-rendered whether they directly use the comment property or not, as the entire object reference has changed.
I came across https://hookstate.js.org/ library which looks awesome. However, I found that this doesn't work when the state is an instance of a class with methods. The proxied object has methods but without this reference bound properly. I would love to hear someone's solution to this as well.
Thank you!
There's nothing wrong with the general idea of passing down both a value and a setter:
const Parent = () => {
const [stuff, setStuff] = useState('default stuff')
return (
<Child stuff={stuff} setStuff={setStuff} />
)
}
const Child = ({ stuff, setStuff }) => (
<input value={stuff} onChange={(e) => setStuff(e.target.value)} />
)
But more in general, I think your main problem is attempting to use the shape of the POST request as your state structure. useState is intended to be used for individual values, not a large structured object. Thus, at the root level, you would have something more like:
const [author, setAuthor] = useState('AUTHOR')
const [post, setPost] = useState('POST')
const [comment, setComment] = useState('CONTENT')
const onSave = () => {
axios.post('URL', {
author: {
name: author,
post: {
content: post,
comment: {
message: comment,
},
},
},
})
}
And then pass them down individually.
Finally, if you have a lot of layers in between and don't want to have to pass a bunch of things through all of them (also known as "prop drilling"), you can pull all of those out into a context such as:
const PostContext = createContext()
const Root = () => {
const [author, setAuthor] = useState('AUTHOR')
const [post, setPost] = useState('POST')
const [comment, setComment] = useState('CONTENT')
const onSave = useCallback(() => {
axios.post('URL', {
author: {
name: author,
post: {
content: post,
comment: {
message: comment,
},
},
},
})
}, [author, comment, post])
return (
<PostContext.Provider
value={{
author,
setAuthor,
post,
setPost,
comment,
setComment,
}}
>
<Post />
</PostContext.Provider>
)
}
See https://reactjs.org/docs/hooks-reference.html#usecontext for more info
Your example illustrates the case of "prop drilling".
Prop drilling (also called "threading") refers to the process you have to go through to get data to parts of the React Component tree.
Prop drilling can be a good thing, and it can be a bad thing. Following some good practices as mentioned above, you can use it as a feature to make your application more maintainable.
The issue with "prop drilling", is that it is not really that scalable when you have an app with many tiers of nested components that needs alot of shared state.
An alternative to this, is some sort of "global state management" - and there are tons of libraries out there that handles this for you. Picking the right one, is a task for you 👍
I'd recommend reading these two articles to get a little more familiar with the concepts:
Prop Drilling
Application State Management with React
I would use redux instead of doing so much props work, once you create your states with redux they will be global and accesible from other components. https://react-redux.js.org/
Related
Problem: Component render starts to drift from actual state
Desired Output: Component render matches state.
So. I'm going to give a bit of a high-level overview with pseudocode as this issue is quite complex, and then I'll show the code.
I have a main form, and this form has an array of filter-states that are renderable in their own components. These filter-states are a one-to-many relationship with the form. The form has-many filter-states.
form: {
filters: [
filter1,
filter2
]
}
Say you want to remove an item from the state, you would do something like so in the reducer (redux)
state.form.filters.filter(f => f.id != action.payload.id)
All good. The state is updated.
Say, you want to render this state, you would do something like so:
// component code ommited, but say you get your form state from redux into the component
formState.filters.map(filter => <FilterComponent filter={filter}/>
All good. your filters are being injected into the component and everyone is happy
Now. This is where it gets weird pretty quickly.
There is a button on my FilterComponent, that says delete. This delete button goes to the reducer, runs the code to delete the filter from the formstate (as you saw above), and yes, it DOES work. The state gets updated, BUT, the UI (the array of components) starts to drift from the state. The UI shows previously deleted states, and states that should be persisted are not shown (but in the redux tab on chrome, the state is CORRECT...!)
The UI acts as if the array of states is being pop()'d; no matter how you remove the states, it will remove the final state in component render.
Now, for the code.
// This takes a list of filters from the form state and loads them into individual form components
const Filters: NextPage<Filters> = () => {
const formState = useSelector((state: any) => state.form.formState)
// In the hope that state change will force reload components, but no avail
useEffect(() => {
console.log("something has been reloaded")
}, [formState])
return (
<>
{formState.form.map((filter, i) => {
return <FilterForm defaultState={filter} key={i} index={i} />
})}
</>
);
};
export default Filters;
The form for these individual states:
Please note, this is obviously redacted a lot but the integral logic is included
const FilterForm: NextPage<FilterFormProps> = ({ defaultState, index }) => {
const formState = useSelector((state: any) => state.form.formState)
// Local component state; there are multiple forms so the state should be localised
const [FilterState, setFilterState] = useState(defaultState)
const handleDelete = (e) => {
dispatch(deleteFilter(filterState.id))
}
const updateParentState = async () => {
dispatch(updateForm(filterState))
}
useEffect(() => {
updateParentState()
}, [filterState])
return (
<CloseButton position="absolute" right="0" top="25px" onClick={handleDelete} name={filterState.id} />
<Input
name="filter_value"
onChange={handleOnChange} // does standard jazz
value={filterState.filter_value} // standard jazz again
/>
)
}
Now what happens is this: if I click delete, redux updates the correct state, but the components display the deleted state input. Ie, take the following:
filter1: {filter_value: "one"}
filter2: {filter_value: "two"}
filter3: {filter_value: "three"}
these filters are rendered in their own forms.
Say, I click delete on filter1.
filter1 will be deleted from redux, but the UI will show two forms: one for filter1 and one for filter2.
This drift from UI to state baffles me. Obviously I am doing something wrong, can someone spot what it is?!
So, I fixed the issue.
As it turns out, there isnt really an explanation for why the above behaved as it does, but it does warrant for a better implementation.
The issue was as follows; the redux state was conflicting with the local state of the rendered components it was injected in. Why it did, is another story. Somehow, while injecting the redux state into the component and assigning it to the local state, the states went a bit haywire and drifted apart.
The solution was to get rid of the local state (filterState), the updateParentState function call and rather to update the localised state directly through the parent state that it resides in.
The new component looked something like the following:
const FilterForm: NextPage<FilterFormProps> = ({ state, index }) => {
const handleDelete = (e) => {
dispatch(deleteFilter(filterState.id))
}
const handleChange = (e) => {
dispatch(updateFormFilterState({ ...state, [e.target.name]: e.target.value }))
}
return (
<CloseButton position="absolute" right="0" top="25px" onClick={handleDelete} />
<Input
name="filter_value"
onChange={handleChange}
value={state.filter_value}
/>
)
}
Hope this answer helps someone with the same issue as me.
Issue
I'm looking for the most optimal way to fetch data using useEffect() when the fetch function is used in more than one place.
Situation
Currently, I have a parent component (ItemContainer) and a child component (SearchBar). ItemContainer should fetch the all the possible list of items using getItemList() functions. I'm executing this function within the useEffect() during the first render, and also passing it down to SearchBar component, so that when a user submits a search term, it will update itemList state by triggering getItemList() in ItemContainer.
This actually works just as I expected. However, my issue is that
I'm not really sure whether it is okay to define getItemList() outside the useEffect() in this kind of situation. From what I've been reading (blog posts, react official docs) it is generally recommended that data fetching function should be defined inside the useEffect(), although there could be some edge cases. I'm wondering if my case applies as this edge cases.
Is it okay to leave the dependency array empty in useCallback? I tried filling it out using searchTerm, itemList, but none of them worked - and I'm quite confused why this is so.
I feel bad that I don't fully understand the code that I wrote. I would appreciate if any of you could enlighten me with what I'm missing here...
ItemContainer
const ItemContainer = () => {
const [itemList, setItemList] = useState([]);
const getItemList = useCallback( async (searchTerm) => {
const itemListRes = await Api.getItems(searchTerm);
setItemList(itemListRes)
}, []);
useEffect(() => {
getItemList();
}, [getItemList]);
return (
<main>
<SearchBar search={getItemList} />
<ItemList itemList={itemList} />
</main>
)
}
SearchBar
const SearchBar = ({ search }) => {
const [searchTerm, setSearchTerm] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
search(searchTerm);
setSearchTerm('');
}
const handleChange = (e) => {
setSearchTerm(e.target.value)
}
return (
<form onSubmit={handleSubmit}>
<input
placeholder='Enter search term...'
value={searchTerm}
onChange={handleChange}
/>
<button>Search</button>
</form>
)
}
Here are my answers.
Yes, it is okay. What's inside useCallback is "frozen" respect to
the many ItemConteiner function calls that may happen. Since the
useCallback content accesses only setItemList, which is also a
frozen handler, there'll be no problems.
That's also correct, because an empty array means "dependent to
nothing". In other words, the callback is created once and keeps
frozen for all the life of the ItemContainer.
Instead, this is something weird:
useEffect(() => {
getItemList();
}, [getItemList]);
It works, but it has a very little sense. The getItemList is created once only, so why make an useEffect depending to something never changes?
Make it simpler, by running once only:
useEffect(() => {
getItemList();
}, []);
This function component has a template method that calls onChangeHandler, which accepts a select value and updates state. The problem is, state does not update until after the render method is called a second time, which means the value of selected option is one step ahead of the state value of selectedRouteName.
I know there are lifecycle methods in class components that I could use to force a state update, but I would like to keep this a function component, if possible.
As noted in the code, the logged state of selectedRouteDirection is one value behind the selected option. How can I force the state to update to the correct value in a functional component?
This question is not the same as similarly named question because my question asks about the actual implementation in my use case, not whether it is possible.
import React, { useState, Fragment, useEffect } from 'react';
const parser = require('xml-js');
const RouteSelect = props => {
const { routes } = props;
const [selectedRouteName, setRouteName] = useState('');
const [selectedRouteDirection, setRouteDirection] = useState('');
//console.log(routes);
const onChangeHandler = event => {
setRouteName({ name: event.target.value });
if(selectedRouteName.name) {
getRouteDirection();
}
}
/*
useEffect(() => {
if(selectedRouteName) {
getRouteDirection();
}
}); */
const getRouteDirection = () => {
const filteredRoute = routes.filter(route => route.Description._text === selectedRouteName.name);
const num = filteredRoute[0].Route._text;
let directions = [];
fetch(`https://svc.metrotransit.org/NexTrip/Directions/${num}`)
.then(response => {
return response.text();
}).then(response => {
return JSON.parse(parser.xml2json(response, {compact: true, spaces: 4}));
}).then(response => {
directions = response.ArrayOfTextValuePair.TextValuePair;
// console.log(directions);
setRouteDirection(directions);
})
.catch(error => {
console.log(error);
});
console.log(selectedRouteDirection); // This logged state is one value behind the selected option
}
const routeOptions = routes.map(route => <option key={route.Route._text}>{route.Description._text}</option>);
return (
<Fragment>
<select onChange={onChangeHandler}>
{routeOptions}
</select>
</Fragment>
);
};
export default RouteSelect;
Well, actually.. even though I still think effects are the right way to go.. your console.log is in the wrong place.. fetch is asynchronous and your console.log is right after the fetch instruction.
As #Bernardo states.. setState is also asynchronous
so at the time when your calling getRouteDirection();, selectedRouteName might still have the previous state.
So to make getRouteDirection(); trigger after the state was set.
You can use the effect and pass selectedRouteName as second parameter (Which is actually an optimization, so the effect only triggers if selectedRouteName has changed)
So this should do the trick:
useEffect(() => {
getRouteDirection();
}, [selectedRouteName]);
But tbh.. if you can provide a Stackblitz or similar, where you can reproduce the problem. We can definitely help you better.
setState is asynchronous! Many times React will look like it changes the state of your component in a synchronous way, but is not that way.
I'm using React and Redux in my web app.
In the login page, I have multiple fields (inputs).
The login page in composed from multiple components to pass the props to.
I was wondering how should I pass the props and update actions.
For example, lets assume I have 5 inputs in my login page.
LoginPage (container) -> AuthenticationForm (Component) -> SignupForm (Component)
In the LoginPage I map the state and dispatch to props,
and I see 2 options here:
mapStateToProps = (state) => ({
input1: state.input1,
...
input5: state.input5
})
mapDispatchToProps = (dispatch) => ({
changeInput1: (ev) => dispatch(updateInput1(ev.target.value))
...
changeInput5: (ev) => dispatch(updateInput5(ev.target.value))
})
In this solution, I need to pass a lot of props down the path (the dispatch actions and the state data).
Another way to do it is like this:
mapStateToProps = (state) => ({
values: {input1: state.input1, ..., input5: state.input5}
})
mapDispatchToProps = (dispatch) => ({
update: (name) => (ev) => dispatch(update(name, ev.target.value))
})
In this solution, I have to keep track and send the input name I want to update.
How should I engage this problem?
It seems like fundamental question, since a lot of forms have to handle it,
but I couldn't decide yet what would suit me now and for the long run.
What are the best practices?
I think best practice would be to handle all of this logic in the React component itself. You can use component's state to store input's data and use class methods to handle it. There is good explanation in React docs https://reactjs.org/docs/forms.html
You probably should pass data in Redux on submit. Ether storing whole state of the form as an object, or not store at all and just dispatching action with api call.
TL;DR. it's a more 'general' coding practice. But let's put it under a react-redux context.
Say if you go with your first approach, then you will probably have 5 actionCreators as:
function updateInput1({value}) { return {type: 'UPDATE_INPUT1', payload: {value}} }
...
function updateInput5({value}) { return {type: 'UPDATE_INPUT5', payload: {value}} }
Also if you have actionTypes, then:
const UPDATE_INPUT1 = 'UPDATE_INPUT1'
...
const UPDATE_INPUT5 = 'UPDATE_INPUT5'
The reducer will probably look like:
function handleInputUpdate(state = {}, {type, payload: {value}}) {
switch (type) {
case UPDATE_INPUT1: return {..., input1: value}
...
case UPDATE_INPUT5: return {..., input5: value}
default: return state
}
}
What's the problem? I don't think you're spreading too many props in mapStateToProps/mapDispatchToProps, Don't repeat yourself!
So naturally, you want a more generic function to avoid that:
const UPDATE_INPUT = 'UPDATE_INPUT'
function updateInput({name, value}) { return {type: UPDATE_INPUT, payload: {name, value}} }
function handleInputUpdate(state = {inputs: null}, {type, payload: {name, value}}) {
switch (type) {
case UPDATE_INPUT: return {inputs: {...state.inputs, [name]: value}}
default: return state
}
}
Finally, the "selector" part, based upon how the state was designed, get component's props from it would be fairly trivial:
function mapStateToProps(state) { return {inputs: state.inputs} }
function mapDispatchToProps(dispatch) { return {update(name, value) { dispatch(updateInput(name, value)) } }
In summary, it's not necessarily a redux/react problem, it's more how you design app state, redux just offers you utilities and poses some constraints to enable "time traveling" (state transitions are made explicit within a mutation handler based on a separate action).
Best practice to handle this problem is having a local state on your Form Component and managing it locally because I believe it's not a shared state. onSubmit you could dispatch your action passing down the state to the action which is required in making an API call or posting it to your server.
If you try to keep updating your store as the user types, it will keep dispatching the action which might cause problems in future. You read more here Handling multiple form inputs in react
So I just switched to using stateless functional components in React with Redux and I was curious about component lifecycle. Initially I had this :
// actions.js
export function fetchUser() {
return {
type: 'FETCH_USER_FULFILLED',
payload: {
name: 'username',
career: 'Programmer'
}
}
}
Then in the component I used a componentDidMount to fetch the data like so :
// component.js
...
componentDidMount() {
this.props.fetchUser()
}
...
After switching to stateless functional components I now have a container with :
// statelessComponentContainer.js
...
const mapStateToProps = state => {
return {
user: fetchUser().payload
}
}
...
As you can see, currently I am not fetching any data asynchronously. So my question is will this approach cause problems when I start fetching data asynchronously? And also is there a better approach?
I checked out this blog, where they say If your components need lifecycle methods, use ES6 classes.
Any assistance will be appreciated.
Firstly, don't do what you are trying to to do in mapStateToProps. Redux follows a unidirectional data flow pattern, where by component dispatch action, which update state, which changes component. You should not expect your action to return the data, but rather expect the store to update with new data.
Following this approach, especially once you are fetching the data asynchronously, means you will have to cater for a state where your data has not loaded yet. There are plenty of questions and tutorials out there for that (even in another answer in this question), so I won't worry to put an example in here for you.
Secondly, wanting to fetch data asynchronously when a component mounts is a common use case. Wanting to write nice functional component is a common desire. Luckily, I have a library that allows you to do both: react-redux-lifecycle.
Now you can write:
import { onComponentDidMount } from 'react-redux-lifecycle'
import { fetchUser } from './actions'
const User = ({ user }) => {
return // ...
}
cont mapStateToProps = (state) => ({
user = state.user
})
export default connect(mapStateToProps)(onComponentDidMount(fetchUser)(User))
I have made a few assumptions about your component names and store structure, but I hope it is enough to get the idea across. I'm happy to clarify anything for you.
Disclaimer: I am the author of react-redux-lifecycle library.
Don't render any view if there is no data yet. Here is how you do this.
Approach of solving your problem is to return a promise from this.props.fetchUser(). You need to dispatch your action using react-thunk (See examples and information how to setup. It is easy!).
Your fetchUser action should look like this:
export function fetchUser() {
return (dispatch, getState) => {
return new Promise(resolve => {
resolve(dispatch({
type: 'FETCH_USER_FULFILLED',
payload: {
name: 'username',
career: 'Programmer'
}
}))
});
};
}
Then in your Component add to lifecycle method componentWillMount() following code:
componentDidMount() {
this.props.fetchUser()
.then(() => {
this.setState({ isLoading: false });
})
}
Of course your class constructor should have initial state isLoading set to true.
constructor(props) {
super(props);
// ...
this.state({
isLoading: true
})
}
Finally in your render() method add a condition. If your request is not yet completed and we don't have data, print 'data is still loading...' otherwise show <UserProfile /> Component.
render() {
const { isLoading } = this.state;
return (
<div>{ !isLoading ? <UserProfile /> : 'data is still loading...' }</div>
)
}