YungOne
YungOne

Reputation: 133

How to update state while in a child component?

I have a component called Blocks that gets data from the backend and maps them all to a child component called Block. I want to be able to update the Block name from within the child but how would I get this change to occur in the parent?

Blocks.js

const Blocks = ({ updateBlocks }) => {
    const [blocks, setBlocks] = useState([])

    useEffect(() => {
        const getBlocks = async () => {
            const blocksFromServer = await fetchBlocks()
            setBlocks(blocksFromServer)
        };

        getBlocks()
    }, []);

    const fetchBlocks = async () => {
        const res = await fetch("/api/getblocks")
        const data = await res.json()

        return data
    }

    return (
        <form onSubmit={updateBlocks}>
            {blocks.map((block) => (
                <Block key={block.id} block={block} />
            ))}

            <input type="submit" value="update"></input>
        </form>
    )
}

export default Blocks

Block.js

import React from "react"
import { useState } from "react"

const Block = ({ block }) => {
    const [topic, setTopic] = useState('')

    return (
        <div className="block">
            <input type="text" name="topic" value={block.topic} onChange={}></input>
            <h4>{block.start_time} - {block.end_time}</h4>
            <hr class="dashed" />
        </div>
    )
}

export default Block

Upvotes: 1

Views: 58

Answers (3)

Aifos Si Prahs
Aifos Si Prahs

Reputation: 363

Blocks.js

const Blocks = ({ updateBlocks }) => {
    const [blocks, setBlocks] = useState([])

    useEffect(() => {
        const getBlocks = async () => {
            const blocksFromServer = await fetchBlocks()
            setBlocks(blocksFromServer)
        };

        getBlocks()
    }, []);

    const fetchBlocks = async () => {
        const res = await fetch("/api/getblocks")
        const data = await res.json()

        return data
    }

    return (
        <form onSubmit={updateBlocks}>
            {blocks.map((block, i) => (
                <Block onBlockChange={(newBlock) => setBlocks(blocks => blocks.splice(i, newBlock))} key={block.id} block={block} />
            ))}

            <input type="submit" value="update"></input>
        </form>
    )
}

export default Blocks

Block.js

import React from "react"
import { useState } from "react"

const Block = ({ onBlockChange, block }) => {
    return (
        <div className="block">
            <input type="text" name="topic" value={block.topic} onChange={(e) => onBlockChange({ topic: e.target.value, ...block})}></input>
            <h4>{block.start_time} - {block.end_time}</h4>
            <hr class="dashed" />
        </div>
    )
}

export default Block

Upvotes: 1

Andy
Andy

Reputation: 63524

You "lift state up" by passing down a reference to a function that updates state into Block. When an input changes its listener calls the function. We grab the name and the value from the input, and update the state with the new data.

For convenience I'm passing in a sample data set.

const { useState } = React;

function Blocks({ data }) {

  // Initialise state with the data
  const [ blocks, setBlocks] = useState(data);

  // The handler destructures the name and value from
  // the changed input, copies the state, finds the index of the
  // block, updates that object's topic, and then updates
  // the state
  function handleChange(e) {
    const { name, value } = e.target;
    const copy = [...blocks];
    const index = copy.findIndex(block => block.name === name);
    copy[index].topic = value;
    setBlocks(copy);
  }

  // Shows the updated state
  function handleClick() {
    console.log(JSON.stringify(blocks));
  }

  // Pass the handler down to each
  // block component
  return (
    <div>
      {blocks.map(block => {
        return (
          <Block
            key={block.id}
            block={block}
            handleChange={handleChange}
          />
        );
      })}
      <button onClick={handleClick}>View state</button>
    </div>
  );

}

// `Block` accepts the data, and the handler reference,
// adds the reference to the `onChange` listener, and the
// block topic to the value (controlled component)
function Block({ block, handleChange }) {
  return (
    <div className="block">
      <label>Topic</label>
      <input
        type="text"
        name={block.name}
        placeholder="Add topic"
        value={block.topic}
        onChange={handleChange}
      />
    </div>
  );
}

const data = [
  { id: 1, name: 'block1', topic: 'topic1' },
  { id: 2, name: 'block2', topic: 'topic2' },
  { id: 3, name: 'block3', topic: 'topic3' }
];

ReactDOM.render(
  <Blocks data={data} />,
  document.getElementById('react')
);
label { margin-right: 0.2em; }
button { margin-top: 1em; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Upvotes: 1

Matthew Herbst
Matthew Herbst

Reputation: 31963

There are many ways to handle shared state, though for a minimal change in your code, you could pass the child Block components an update function to use:

const handleNameChange = useCallback((blockId, newName) => {
    const updatedBlocks = blocks.map((block) => {
        if (block.id === blockId) {
            block.name = name;
        }

        return block;
    });
  
    setBlocks(updatedBlocks);
}, [blocks]);

// ...

{blocks.map((block) => (
    <Block key={block.id} block={block} onNameChange={handleNameChange} />
))}

In Block:

const { block, onNameChange } = props;

const handleOnChange = useCallback((event) => {
  onNameChange(block.id, event.target.value);
}, [block.id]);

<input type="text" name="topic" value={block.topic} onChange={handleOnChange} />

Upvotes: 1

Related Questions