How to manage multiple states in React Native - javascript

I have a Screen component called "EditProfile" which contains three forms:
The UserInformationForm, which contains several information of the user as states.
The ChangePasswordForm, which contains the password and repeat password of the user as states
The ChangeAvatarForm, which contains the avatar uri as state.
All these components have a common parent, the screen EditProfile, as I said. In this screen I have the logic to upload all the information to a DB.
I need to access each child information, so imagine that I have all the forms states in the parent. I think that having multiple states (more than 20) in the parent is insane and can affect performance... So I have thought to wrap each child with the hook useImperativeHandle, just to retrieve all this data without having to save all the states in the parent and avoid unnecesary re-renders... I don't know if this is a good practice as in the documentation says to avoid this type of behaviour and use the common data flow...
Is it normal to have all those states in the parent? Can I use the useImperativeHandle hook in this situation? Any patterns?
Thank you.
UPDATE
I mean, something like this:
ChangeAvatarForm.js
const ChangeAvatarForm = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
getAvatar: () => avatar,
}));
const [avatar, setAvatar] = useState(props.avatar);
...
EditProfile.js (The parent screen which have all the forms)
export default function EditProfile(props) {
const { information } = props; // Current user information
// References to each form
const avatarFormRef = useRef(null);
const userInformationFormRef = useRef(null);
const passwordFormRef = useRef(null);
const submitChanges = () => {
// Get the new avatar
const newAvatar = avatarFormRef.current.getAvatar();
...
}
...

Related

React Hooks - Global state without context

I am implementing a hook "useUserPosts", which is supposed to be used in several routes of my application.
As I already have a context "PostsContext" which re-renders my Cards when data changes (totalLikes, totalComments, descriptions, ...), I have decided to avoid creating another one called "UserPostsContext" which purpose is to return the user posts array.
I know, why not to use the PostsContext instead?...
The answer is that, in PostsContext, to avoid performance issues, I am storing a map (key, value), in order to get/update the posts dynamic data in O(1), something which is only useful in my components (so, it is used to synchronize Cards basically)
Is it possible/a common practice in React to create hooks that handles global states without using the Context API or Redux?
I mean, something like
// Global State Hook
const useUserPosts = (() => {
const [posts, setPosts] = useState({});
return ((userId) => [posts[id] ?? [], setPosts]);
})();
// Using the Global State Hook
function useFetchUserPosts(userId) {
const [posts, setPosts] = useUserPosts(userId);
const [loading, setLoading] = useState(!posts.length);
const [error, setError] = useState(undefined);
const cursor = useRef(new Date());
const hasMoreToLoad = useRef(false);
const isFirstFetch = useRef(true);
const getUserPosts = async () => {
// ...
}
return { posts, loading, error, getUserPosts };
}
Note: my purpose with this is to:
1. Reproduce some kind of cache
2. Synchronize the fetched data of each stack screen that is mounted in order to reduce backend costs
3. Synchronize user posts deletions
Even if I think creating a new global state is the best solution, if you really want to avoid it, you could create your own as follow :
export class AppState {
private static _instance: AppState;
public state = new BehaviorSubject<AppStateType>({});
/**
* Set app state without erasing the previous values (values not available in the newState param)
* */
public setAppState = (newState: AppStateType) => {
this.state.next({ ...this.state, ...newState });
};
private constructor() {}
public static getInstance(): AppState {
if (!AppState._instance) {
AppState._instance = new AppState();
}
return AppState._instance;
}
}
With this kind of type :
export type AppStateType = {
username?: string;
isThingOk?: boolean;
arrayOfThing?: Array<MyType>;
...
}
And use it this way :
const appState = AppState.getInstance();
...
...
appState.setAppState({ isThingOk: data });
...
...
appState.state.subscribe((state: AppStateType) => {// do your thing here});
Not sure this is the best way to create a state of your own but it works pretty well. Feel free to adapt it to your needs.
I can recommand you to use some light state management library as zustand: https://github.com/pmndrs/zustand.
You can with this library avoid re-render and specify to re-render just want the data you want change or change in certain way with some function to compare old and new value.

Is it correct to use two different useState hook to store an array and a filtered array for passing to components?

I am currently porting my Pokemon project to React as I just learned the basics of React just a couple of weeks ago and want to see how well I can adapt it. Right now, the way I have my code architectured is to use two different useState hooks, one for the original array fetched from the PokeAPI and is only set once. This original array is also passed into my form object, which filters it according to a few form elements such as a Pokemon type or the Pokemon's name. And another useState hook is used to keep track of the filteredList which is what gets rendered to the website using the Array.map() function.
const [pokemonList, setPokemonList] = useState([]);
const [filteredList, setPokemonFilteredList] = useState([]);
Here's the useEffect() hook where we fetch and set the states
useEffect(() => {
const getPokemon = async () => {
const list = await fetchPokemon();
setPokemonList(list);
setPokemonFilteredList(list);
};
And finally the pokemonList state variable and setPokemonFilteredList methods get passed into the <PokemonSearchForm>
<PokemonSearchForm pokemonList={pokemonList} setPokemonList={setPokemonFilteredList} />
So as my question title suggests, is the way I use two different useState() 'correct'? Maybe a different way is for the child component to access pokemonList variable? But I believe this may be an anti-pattern.
It is better practice to not maintain duplicate or, in this case, derived state because you may run into divergence. For example, if your original pokemon data got updated, how would you make sure your filtered data got updated and then the filters re-applied? It gets hairy very fast.
The preferred alternative is to maintain the original data and filters in state and then compute the derived state (in this case, filter the list) during render.
function App() {
const [pokemonList, setPokemonList] = useState([]);
// Some default filter state
const [filters, setFilters] = useState({
types: ["any"],
search: ""
});
const filteredList = pokemonList.filter((pokemon) => {
// Filter logic here
});
return <PokemonSearchForm setFilters={setFilters} />
}
I would refactor this in a few different ways:
Keep the filter state in your parent component, the child component will simply notify it when those change.
Ditch the useState for useMemo which computes a value every time its dependencies change.
function YourComponent() {
const [filters, setFilters] = useState({});
const [pokemons, setPokemons] = useState([]);
useEffect(
() => {
const list = await fetchPokemon();
setPokemons(list);
},
[]
);
// This will run each time `filters` or `pokemons` change.
const filteredPokemons = useMemo(
() => {
return pokemons.filter((pokemon) => {
// Perform any filtering logic you may have,
// based on the filters set by the child component.
if (filters.name) {
return pokemon.name.includes(filters.name);
}
// etc...
});
},
[filters, pokemons]
);
return (
<PokemonSearchForm
pokemons={filteredPokemons}
onChange={setFilters}
/>
);
}

React hook use class object as useState

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/

react / functional component / props changed / getDerivedStateFromProps

Lets say I'm doing a simple CRUD app in react. My functional component is basically just the form.
In the CREATE case I pass in an empty object via props
In the UPDATE case I pass in an object with the values via props (I got the data in the parent component with an API call)
I looks like this:
const MyForm = (props) => {
const [myValues, setMyValues] = useState(props.myValues);
const [errors, setErrors] = useState(0);
(...)
}
In the UPDATE case, I run (of course) into the issue that props.myValues is still empty when the component is mounted, and not set again (updated) when the api call from the parent component has finished thus leaving the form values empty.
Using a class component, I'd solve that with getDerivedStateFromProps(). Is there anything like that in a functional component? Or am I doing this wrong from the beginning? Thanks for any advice!
Yes, you can of course use useEffect.
In your case the code should look like this:
const MyForm = (props) => {
const [myValues, setMyValues] = useState(props.myValues);
const [errors, setErrors] = useState(0);
useEffect(() => {
setMyValues(props.myValues);
}, [props.myValues]);
}
Another way to do this is the Fully uncontrolled component with a key strategy. If you go that route, your component stays the same:
const MyForm = (props) => {
const [myValues, setMyValues] = useState(props.myValues);
const [errors, setErrors] = useState(0);
(...)
}
But there is one extra piece to its usage:
<MyForm myValues={myValues} key={myValues} />
Using the key prop like this means a new instance of this component will be created if myValues changes, meaning the default value you have provided to your internal myValues state will be reset to whatever the new values are.
This avoids the double render you get if you useEffect, but comes at the cost of a new component instance. I don't find this to be a problem if you do it at the individual input level, but you may not want to rebuild your whole form.
When you write:
const [myValues, setMyValues] = useState(props.myValues);
then myValues props is only used for initialize the myValues state.
Dan Abramov relates to this point in his latest article.
I would suggest:
1. Rename myValues prop to initialValues
2. Call the api after submit, and change your state according to the result.
It will look like:
const MyForm = (props) => {
const { initialValues, onSubmit } = this.props
const [myValues, setMyValues] = useState(initialValues);
...
const handleSubmit = () => {
// Assume that onSubmit is a call to Fetch API
onSubmit(myValues).then(response => response.json())
.then(updatedValues => setMyValues(updatedValues))
}
}

pass props to selectors to filter based on that props

I need to pass props to selectors so that i can fetch the content of the clicked item from the selectors. However i could not pass the props. I tried this way but no success
const mapStateToProps = createStructuredSelector({
features: selectFeatures(),
getFeatureToEditById: selectFeatureToEditById(),
});
handleFeatureEdit = (event, feature) => {
event.preventDefault();
console.log("feature handle", feature);
const dialog = (
<FeatureEditDialog
feature={feature}
featureToEdit={selectFeatureToEditById(feature)}
onClose={() => this.props.hideDialog(null)}
/>
);
this.props.showDialog(dialog);
};
selectors.js
const selectFeatureState = state => state.get("featureReducer");
const selectFeatureById = (_, props) => {
console.log("props", _, props); #if i get the id of feature here
# i could then filter based on that id from below selector and show
# the result in FeatureEditDialog component
};
const selectFeatureToEditById = () =>
createSelector(
selectFeatureState,
selectFeatureById,
(features, featureId) => {
console.log("features", features, featureId);
}
);
Here is the gist for full code
https://gist.github.com/MilanRgm/80fe18e3f25993a27dfd0bbd0ede3c20
Simply pass both state and props from your mapStateToProps to your selectors.
If you use a selector directly as the mapStateToProps function, it will receive the same arguments mapState does: state and ownProps (props set on the connected component).
A simple example:
// simple selector
export const getSomethingFromState = (state, { id }) => state.stuff[id]
// reselect selector
export const getStuff = createSelector(
getSomethingFromState,
(stuff) => stuff
)
// use it as mapDispatchToProps
const mapDispatchToProps = getSomethingFromState
const MyContainer = connect(mapDispatchToProps)(MyComponent)
// and set id as an own prop in the container when rendering
<MyContainer id='foo' />
However you're doing some weird things like mapping a selector to reuse it later. It doesn't work that way, at least it's not intended to be used that way.
You use selectors to retrieve slices of your state and pass it as props to your connected components. Whenever the state changes, your selectors will be re-run (with some caching thanks to reselect). If something the component is actually retrieving from Redux has changed indeed, it will re-render.
So your FeatureEditDialog component should be connected as well, and should be capable of retrieving anything it needs from the Redux state, just by using props (which feature, which id, so on) in its own connect call.
this.props.showDialog(dialog); is a big code smell as well. ;)

Categories