4

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:

  1. An empty post with ID, "ABC", is selected. Update selectedPosts context
  2. User uploads a file and we wait for CDN to return image url
  3. Update loading context of post ABC (loading == true) (receives image url)
  4. Update posts context at ABC
  5. Update media context at ABC
  6. Update loading context of post ABC (loading == false)
  7. 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.

adrayv
  • 359
  • 1
  • 3
  • 8

1 Answers1

0

Old post, but am assuming that selectedPosts and loadingPosts are filtered posts objects.

Personally I would probably not have them as separate, but filter of posts which are marked loading / selected and upload those in code via some action?

I.e. state has list of posts, with loading / selected props and code does the filter on posts to upload - are you over complicating it? Presuming with some lookup one to many / many to many? on state for media to create the association? What did you end up doing? I would be using useReducer and dispatching updates to state, not sure context is needed unless its deeply nested.

Jeremy
  • 1,170
  • 9
  • 26