I'm constructing a React App that is basically a photo sharing app.
Here's some use cases:
- User can upload photos and videos
- User can see the photos in a list view
- User can reorder the photos in the list
- User can select photos from list and performs actions based on their selection
- Users can attribute properties to these photos, such as message, title, etc. Lets call an image + its properties a Post
Here's some major architectural components:
- A CDN service to upload, host, and transform image and video creation
- A backend application paired with a DB for persistent storage
I'm looking for a good way to organize all this data in state. One thought I had was to break up the state into separate, simple data structures.
Roughly this:
posts <Array> maps Post index to Post ID
media <Object> maps Post ID to Image Urls
selectedPosts <Object> maps Post ID to Boolean
loadingPosts <Object> maps Post ID to Boolean
So here we have four data structures.
- posts: Determines what posts are in state and in what order
- media: Attributes Post IDs to Image URLs
- selectedPosts: Determines what posts are selected
- loadingPosts: Determines if a given post is loading or not
I'm surfacing these via four React Contexts
Breaking up state into separate contexts makes it really easy for dependent components to subscribe to exactly the state they need. For example:
import React from 'react'
import useMedia from '/hooks/media'
export default ({ postId }) => {
const { media } = useMedia() // useMedia uses useContext under the hood
const imageForThisPost = media[postId]
return (
<Image src={imageForThisPost}/>
)
}
What I really like about this is that this component gets exactly the state it needs from global state and really only has one reason to re-render (pretend i'm using useMemo or something). I've worked with some tough React Redux web apps in the past where every component re-rendered on any state change because all the state was in one data structure (albeit memoized selectors could have fixed this).
Problems arise when it comes to use cases that impact multiple contexts. Take uploading an image as an example:
The sequence of events to upload a photo looks like this:
- An empty post with ID, "ABC", is selected. Update selectedPosts context
- User uploads a file and we wait for CDN to return image url
- Update loading context of post ABC (loading == true) (receives image url)
- Update posts context at ABC
- Update media context at ABC
- Update loading context of post ABC (loading == false)
- Deselect post ABC. Update selectedPosts context
Long, intricate, async sequences like this are tough to deal with, encapsulate, reuse, and test.
What's a better way to organize state for medium sized web applications like this with potentially long sequences of async actions and somewhat complex state?
Wishlist:
- Easy to control re-renders
- Easy to add extend/change app functionality (not a fan of huge deeply nested data structures)
- Easy to test
- Does not use Redux (but useReducer is fine) (I just don't like the huge overhead that comes with redux)
Anyone have any thoughts?
I know one way might be to emulate Redux using useReducer, actions, and selectors. And thankfully dispatch
is a stable function identity in React. Idk, I just really don't like dealing with big, deeply nested objects. When product requirements change, those are such a pain to deal with because the entire application depends on a particular schema shape.