0

I have a strange issue with passing a function as a prop in my React app. Code as follows:

const NewPage = () => {
    const [blocks, setBlocks] = useState([]);
    const [needsShowImageModal, setNeedsShowImageModal] = useState(false);

    const textButtonHandler = () => {
        const key = randomInt(0, 1000000000);
        const array = blocks.concat({ key, deleteButtonHandler: deleteButtonHandler });
        setBlocks(array);
    };

    function deleteButtonHandler(blockKey) {
        // Test functionality, if one text field was added arrray size should
        // be 1
        console.log(blocks.length);
    }
    return (
        <div>
            <ImageModal 
                show={needsShowImageModal}
                onHide={() => setNeedsShowImageModal(false)}
                insertButtonHandler={insertImageHandler}
            />
            <div className="d-flex">
                <NewPageSidebar
                    textButtonHandler={textButtonHandler}
                    imageButtonHandler={imageButtonHandler}
                    spacingButtonHandler={spacingButtonHandler}
                />
                <NewPageContent blocks={blocks} />
            </div>
        </div>
    );
};

export default NewPage;

When text button handler is called (a button press) I add a new data model to the blocks array. I have another button handler deleteButtonHandler that is passed to the NewPageContent component (inside the data model). NewPageContent:

const NewPageContent = ({ blocks }) => {
    return (
        <div className="new-page-content-container border mr-5 ml-5 p-3">
            {blocks.map(block =>
                <TextBlock 
                    key={block.key} 
                    blockKey={block.key}
                    deleteButtonHandler={block.deleteButtonHandler}
                />
            )}
        </div>
    );
};

NewPageContent.propTypes = {
    blocks: PropTypes.arrayOf(PropTypes.element)
};

export default NewPageContent;

And finally this handler is passed to TextBlock:

const TextBlock = ({ deleteButtonHandler, blockKey }) => {
    const [editorState, setEditorState] = useState(
        () => EditorState.createEmpty(),
    );

    const toolbarClickHander = (buttonType, e) => {
        e.preventDefault();
        switch (buttonType) {
            case 'delete':
                // Delete button handler called here
                deleteButtonHandler(blockKey);
                break;
            default:
                break;
        }
    };

    return(
        <div className='text-block'>
            <TextBlockToolbar clickHandler={toolbarClickHander} />
             <Editor 
                editorState={editorState}
                onChange={setEditorState}
            />
        </div>
    );
    
};

If I add one element to blocks via textButtonHandler the component is rendered on screen as expected. However if I tap the delete button and deleteButtonHandler is called it will log the size of the array as 0, Strangely if I add second element and then tap the delete button for that element if logs the array size as 1. It's almost as if it's taking a snapshot of the blocks state just as the new textButtonHandler is assigned to the prop and not using the actual current state of blocks. Any ideas what I might be doing wrong here? Haven't run into this issue before. Thanks

reymon359
  • 1,240
  • 2
  • 11
  • 34
Kex
  • 8,023
  • 9
  • 56
  • 129
  • Can you throw that in a sandbox? – Domino987 Jun 25 '20 at 07:33
  • Please make a producible [codesandbox](https://codesandbox.io/s/vanilla-react-template-irhcq), see [How to create a Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) – Dennis Vash Jun 25 '20 at 07:35
  • Are you console logging `blocks` *right* after a `setBlocks` call within the same function? – Drew Reese Jun 25 '20 at 07:36
  • @DrewReese no, it's a button press. I know setting state is async but it will already be finished. – Kex Jun 25 '20 at 07:38
  • try to change your `deleteButtonHandler` to arrow function – Jacek Walasik Jun 25 '20 at 07:41
  • 1
    Ok, yeah, I see. Basically each time `NewPage` renders it recreates `textButtonHandler` and attaches as callback. When `textButtonHandler` is invoked it encloses an "instance" of `deleteButtonHandler` which also encloses the current instance of `blocks` *from that render cycle*. When that instance of `deleteButtonHandler` is invoked it logs the value of `blocks.length` from the time of the enclosure. – Drew Reese Jun 25 '20 at 07:48
  • Is there a method to get around this and pass as function that is not an instance? – Kex Jun 25 '20 at 07:50
  • 1
    So you would pass a delete method for each entry. What if you write a general delete function, and just passing in a key, index, or id, and handling the action at the root(where state stored)? – Peter Jun 25 '20 at 07:53
  • @Peter I think that's what I'm already trying to do no? I am passing `deleteButtonHandler` as the general delete function. – Kex Jun 25 '20 at 07:56
  • Yes, but assigned to the data. It could be standalone, and passing as a standalone prop. I think its much more the react way. Also easier to debug – Peter Jun 25 '20 at 08:00
  • Do you mind writing an example as an answer? Not sure I follow – Kex Jun 25 '20 at 08:01
  • Yes. just a moment – Peter Jun 25 '20 at 08:02

1 Answers1

1

Okay. What is happening here:

You passing a function in an object. That object might have an own context, and you tryes to use that function in that object context, what confuses react. (I know that ECMAScript simple objects has no own context, but react might process that data, so might work in different way .)
So, pass each function standalone prop in the child component.

Example: https://codesandbox.io/s/broken-waterfall-vgcyj?file=/src/App.js:0-1491

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [blocks, setBlocks] = useState([
    { key: Math.random(), deleteButtonHandler }
  ]);

  const textButtonHandler = () => {
    const key = Math.random();
    // const array = blocks.concat({
    // key,
    // deleteButtonHandler: deleteButtonHandler
    // });
    setBlocks(prev => prev.concat({ key, deleteButtonHandler }));
  };

  const deleteButtonHandler = blockKey => {
    // Test functionality, if one text field was added arrray size should
    // be 1
    console.log(blocks.length);
  };
  return (
    <div>
      <div className="d-flex">
        <NewPageContent
          deleteButtonHandler={deleteButtonHandler}
          blocks={blocks}
        />
      </div>
      <button onClick={textButtonHandler}>Handler</button>
    </div>
  );
}

const NewPageContent = ({ blocks = [], deleteButtonHandler = () => null }) => {
  return (
    <div className="new-page-content-container border mr-5 ml-5 p-3">
      {blocks.map(block => (
        <TextBlock
          key={block.key}
          blockKey={block.key}
          // deleteButtonHandler={block.deleteButtonHandler}
          deleteButtonHandler={deleteButtonHandler}
        />
      ))}
    </div>
  );
};

const TextBlock = ({ deleteButtonHandler = () => null, blockKey = "" }) => {
  return (
    <div className="text-block">
      {blockKey}
      <span onClick={deleteButtonHandler}>X</span>
    </div>
  );
};

I have consoled out your old solution, to compare it.

Peter
  • 1,280
  • 8
  • 15
  • This makes perfect sense now! Thanks! I'm not sure why I was passing the function in an object. Brain blip! Thanks! – Kex Jun 25 '20 at 08:41
  • Why do you sue `concat` instead of push in this example? – Kex Jun 25 '20 at 08:43
  • Because you should always return a new state, instead of state modification. This https://stackoverflow.com/questions/44572026/difference-between-concat-and-push/44572132#:~:text=The%20push()%20adds%20elements,new%20length%20of%20the%20array.&text=The%20concat()%20method%20is,instead%20returns%20a%20new%20array. And this might help: https://stackoverflow.com/questions/48865710/spread-operator-vs-array-concat – Peter Jun 25 '20 at 08:48
  • If you returning an old state with modification, react will not rerender that array – Peter Jun 25 '20 at 08:51