AweSIM
AweSIM

Reputation: 1703

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!

Upvotes: 2

Views: 2185

Answers (3)

JAM
JAM

Reputation: 6205

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:

Upvotes: 1

Eric Haynes
Eric Haynes

Reputation: 5851

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

Upvotes: 2

Evren
Evren

Reputation: 4410

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/

Upvotes: 1

Related Questions