2 votes

p5.js et React : comment mettre à jour une esquisse à l'intérieur d'un composant lorsque l'un des props du composant est mis à jour ?

Je travaille sur un projet qui intègre p5.js et React. Mon projet consiste en App.js et deux composants enfants : View1.js et View2.js. Je veux passer des informations de View1 à View2 et faire en sorte que cette information change dans le croquis de View 2. Mon problème est que, bien que je puisse passer des données de View 1 à View 2, je ne sais pas comment mettre à jour le croquis avec la nouvelle valeur.

Je pense qu'une partie du problème peut être causée par le fait que l'esquisse dans le composant View 2 est en mode instance, donc une fois qu'elle est initialisée, elle ne change pas. En fait, je cherche un moyen de réinitialiser/réactualiser l'esquisse à l'intérieur d'un composant à chaque fois que l'un des accessoires du composant change de valeur afin que l'esquisse utilise la valeur la plus récente. Je sais que p5-react-wrapper a un moyen de traiter ce problème, mais idéalement, j'aimerais le faire avec la bibliothèque p5.

Quelqu'un sait-il comment faire ? J'ai inclus un exemple de ce que je veux faire ci-dessous (bien que ce soit un peu plus compliqué que cela avec mon projet réel).

App.js

import React, { useState } from 'react';
import View1 from "./components/View1.js";
import View2 from './components/View2.js';

function App() {

  const [data, setData] = useState(0);

  const firstViewToParent = (num) => {setData(num);}

  return (
    <div className="App">
      <View1
        firstViewToParent={firstViewToParent}
      />
      <View2
        dataFromSibling={data}
      />

    </div>
  );
}

export default App;

View1.js

import React, { useEffect } from "react";
import p5 from 'p5';

const View1 = props => {

    const Sketch = (p) => {

        p.setup = () => {
            p.createCanvas(400, 400);
        }

        p.draw = () => {
            p.background(0);
        }

        p.mouseClicked = () => {
            // Passing information from the child up to the parent
            props.firstViewToParent(p.mouseX);

        }
    }

    useEffect(() => {
        new p5(Sketch);
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    return (
        <></>
    );
}

export default View1;

View2.js

import React, { useEffect } from "react";
import p5 from 'p5';

const View2 = props => {

    const Sketch = (p) => {

        p.setup = () => {
            p.createCanvas(400, 400);
        }

        p.draw = () => {
            p.background(255, 0, 0);
            // Want to be able to access updated information from sibling 
           // component inside the sketch
            p.print(props.dataFromSibling);
        }
    }

    useEffect(() => {
        new p5(Sketch);
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    return (
        <></>
    );
}

export default View2;

2voto

Paul Wheeler Points 3696
  1. Vos composants "View" doivent rendre des éléments de conteneur dans lesquels le canevas p5.js doit être inséré.
  2. Parce que vous avez transmis des tableaux vides à la fonction useEffect les appels de fonction, votre View2 n'était pas mis à jour.
  3. En props qui est transmis à la fonction de votre composant est n'a jamais muté. Quand props changer un nouveau props est transmis à un nouvel appel à la fonction de votre composant.
  4. Lorsque les effets doivent être mis à jour, leur fonction de nettoyage est appelée, puis la fonction d'initialisation est à nouveau appelée. Il est donc important de supprimer l'esquisse pendant le nettoyage. En fait, vous devriez toujours le faire, car si votre composant entier est supprimé, vous voulez le faire savoir à p5.js avant que son élément canvas ne soit supprimé du DOM.

Voici un exemple concret :

// This is in place of an import statement
const { useRef, useState, useEffect } = React;

const View1 = props => {
  const containerRef = useRef();

  const Sketch = (p) => {
    p.setup = () => {
      p.print("View1 Initializing");
      p.createCanvas(200, 100);
    }

    p.draw = () => {
      p.background(150);
    }

    p.mouseClicked = () => {
      p.print('click!');
      // Passing information from the child up to the parent
      props.firstViewToParent(p.map(p.mouseX, 0, p.width, 0, 255));
    }
  }

  useEffect(
    () => {
      // make sure the p5.js canvas is a child of the component in the DOM
      new p5(Sketch, containerRef.current);
    },
    // This empty list tells React that this effect never needs to get re-rendered!
    []
  );

  // Note: you're going to want to defined an element to contain the p5.js canvas
  return (
    <div ref={containerRef}></div>
  );
}

const View2 = props => {
  console.log('view2');
  const containerRef = useRef();

  const Sketch = (p) => {
    p.setup = () => {
      p.print("View2 Initializing");
      p.createCanvas(200, 100);
    }

    p.draw = () => {
      p.background(props.dataFromSibling);
    }
  }

  useEffect(
    () => {
      let inst = new p5(Sketch, containerRef.current);

      // Cleanup function! Without this the new p5.js sketches
      // generated with each click will just appear one after the other.
      return () => inst.remove();
    },
    // Let React know that this effect needs re-rendering when the dataFromSibling prop changes
    [props.dataFromSibling]
  );

  return (
    <div ref={containerRef}></div>
  );
}

function App() {
  const [data, setData] = useState(0);

  const firstViewToParent = (num) => {
    console.log('handling firstViewToParent callback');
    setData(num);
  };

  return (
    <div className="App">
      <View1 firstViewToParent={firstViewToParent}/>
      <View2 dataFromSibling={data} />
    </div>
  );
}

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

.App {
  display: flex;
  flex-direction: row;
}

<html>

<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script>
</head>

<body>
  <div id="root"></div>
</body>

</html>

Addendum : Mise à jour sans suppression

J'étais donc curieux de savoir ce qui se passerait si vous vouliez mettre à jour une esquisse p5.js existante qui a été créée à l'aide de la fonction useEffect() sans supprimer et recréer l'ensemble du sketch p5.js. Eh bien, désistement : Je ne suis pas un expert de ReactJS mais je pense que la réponse est oui. En gros, vous devez faire ce qui suit :

  1. Créer une nouvelle variable d'état/méthode de définition pour stocker et définir l'esquisse elle-même.
  2. Dans le cadre de la useEffect qui crée l'esquisse :
    1. Ne créer l'esquisse que si elle n'existe pas encore
    2. Lorsque vous créez l'esquisse, utilisez la méthode set pour la stocker dans la variable state.
    3. Ne pas renvoyer de fonction de nettoyage
  3. Ajouter un autre effet qui est mis à jour lorsque la variable d'état qui stocke une référence à l'esquisse est mise à jour.
    1. Cet effet ne doit rien créer, il doit simplement renvoyer une fonction de nettoyage.
    2. Cette fonction de nettoyage doit supprimer l'esquisse, si elle existe (en utilisant la variable d'état susmentionnée).
  4. Dans votre fonction de création de croquis, créez une variable locale pour stocker les données d'entrée initiales provenant de props
  5. Ajoutez une fonction à votre objet d'esquisse (en attribuant à la fonction p.updateFn = ... ) qui met à jour ladite variable locale
  6. Dans votre useEffect qui crée le croquis, si le croquis existe déjà, appeler ladite fonction de mise à jour avec la nouvelle valeur de props .

Je sais que c'est un peu compliqué, mais voici un exemple :

// This is in place of an import statement
const { useRef, useState, useEffect } = React;

const View1 = props => {
  const containerRef = useRef();

  const Sketch = (p) => {
    p.setup = () => {
      p.print("View1 Initializing");
      p.createCanvas(200, 100);
    }

    p.draw = () => {
      p.background(150);
    }

    p.mouseClicked = (e) => {
      // It turns out that p5.js listens for clicks anywhere on the page!
      if (e.target.parentElement === containerRef.current) {
        p.print('click!');
        // Passing information from the child up to the parent
        props.firstViewToParent(p.map(p.mouseX, 0, p.width, 0, 255));
      }
    }
  }

  useEffect(
    () => {
      // make sure the p5.js canvas is a child of the component in the DOM
      let sketch = new p5(Sketch, containerRef.current);

      return () => sketch.remove();
    },
    // This empty list tells React that this effect never needs to get re-rendered!
    []
  );

  // Note: you're going to want to defined an element to contain the p5.js canvas
  return (
    <div ref={containerRef}></div>
  );
}

const View2 = props => {
  console.log('view2');
  const containerRef = useRef();
  const [sketch, setSketch] = useState(undefined);

  const Sketch = (p) => {
    let bgColor = props.dataFromSibling;
    p.setup = () => {
      p.print("View2 Initializing");
      p.createCanvas(200, 100);
    }

    p.draw = () => {
      p.background(bgColor);
    }

    p.updateBackgroundColor = function(value) {
      bgColor = value;
    }
  }

  useEffect(
    () => {
      if (!sketch) {
        // Initialize sketch
        let inst = new p5(Sketch, containerRef.current);

        setSketch(inst);
      } else {
        // Update sketch
        sketch.updateBackgroundColor(props.dataFromSibling);
      }

      // We cannot return a cleanup function here, be cause it would run every time the dataFromSibling prop changed
    },
    // Let React know that this effect needs re-rendering when the dataFromSibling prop changes
    [props.dataFromSibling]
  );

  useEffect(
    () => {
      // This effect is only responsible for cleaning up after the previous one 
      return () => {
        if (sketch) {
          console.log('removing sketch!');
          // Removing p5.js sketch because the component is un-mounting
          sketch.remove();
        }
      };
    },
    // This effect neds to be re-initialized *after* the sketch gets created
    [sketch]
  );

  return (
    <div ref={containerRef}></div>
  );
}

function App() {
  const [data, setData] = useState(0);
  const [showSketch, setShowSketch] = useState(true);

  const firstViewToParent = (num) => {
    console.log('handling firstViewToParent callback');
    setData(num);
  };

  return (
    <div className="App">
      <View1 firstViewToParent={firstViewToParent}/>

      {showSketch && <View2 dataFromSibling={data} />}
      <button onClick={() => setShowSketch(showSketch ? false : true )}>
        Toggle
      </button>
    </div>
  );
}

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

.App {
  display: flex;
  flex-direction: row;
}

<html>

<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/p5@1.4.0/lib/p5.js"></script>
</head>

<body>
  <div id="root"></div>
</body>

</html>

Si cela va à l'encontre des meilleures pratiques de ReactJS ou sera cassé dans certains scénarios, j'espère que quelqu'un qui connaît React mieux que moi le dira.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X