heinetz: Frage zu REACT

Hallo Forum,

ich treibe mich mittlerweile fast 20 Jahre hier herum und habe so viel gelernt. Mit meinen Fragen rum um HTML und Javascript weiss ich, dass ich hier richtig bin. Mittlerweile werden aber Frameworks und Libraries eingesetzt und ich beschäftige mich gerade mit REACT. Bin ich mit Fragen dazu hier auch richtig?

gruss, heinetz

  1. Hallo heinetz,

    ich treibe mich mittlerweile fast 20 Jahre hier herum und habe so viel gelernt. Mit meinen Fragen rum um HTML und Javascript weiss ich, dass ich hier richtig bin. Mittlerweile werden aber Frameworks und Libraries eingesetzt und ich beschäftige mich gerade mit REACT. Bin ich mit Fragen dazu hier auch richtig?

    Ja.

    Wobei es natürlich vorkommen kann, dass keiner der hier Lesenden eine Antwort weiß. Aber das kann dir natürlich bei jeder anderen Technologie auch passieren.

    LG,
    CK

  2. Tach!

    ich beschäftige mich gerade mit REACT. Bin ich mit Fragen dazu hier auch richtig?

    Warum nicht? Ich kann sie dir zwar nicht beantworten, außer vielleicht methodische Frage, wo man anderenorts vermutlich ähnlich vorgehen würde. Aber vielleicht gibts ja andere Wissende. Probier es einfach.

    dedlfix.

    1. Allright!

      Folgend meine Komponente:

      1. /* eslint-disable jsx-a11y/media-has-caption */
      2. import React, { Component } from 'react';
      3. import PropTypes from 'prop-types';
      4. import classNames from 'classnames';
      5. import MwocAppContext from './MwocAppContext.jsx';
      6. import Overlay from './Overlay/index.jsx';
      7. import ControlPanel from './ControlPanel/index.jsx';
      8. import DragDrop from './DragDrop/index.jsx';
      9. import Hotspots from './Hotspots/index.jsx';
      10. import VideoMI24Source from '../VideoMI24Source';
      11. 
      12. const DEFAULT_STATE = {
      13.   chapter: 0,
      14.   level: 0,
      15.   data: {
      16.     level: {},
      17.     chapter: {}
      18.   },
      19.   dataLoaded: false,
      20.   introStarted: false,
      21.   introEnded: false,
      22.   loopStarted: false,
      23.   aspecRatio: 0
      24. };
      25. 
      26. export default class MwocApp extends Component {
      27.   constructor(props) {
      28.     super(props);
      29.     this.dragdrop = React.createRef();
      30.     this.intro = React.createRef();
      31.     this.loop = React.createRef();
      32. 
      33.     this.state = DEFAULT_STATE;
      34.   }
      35. 
      36.   componentDidMount() {
      37.     // console.log('componentDidMount()');
      38.     this.loadData();
      39.   }
      40. 
      41.   loadData = (settings = { chapter: 0, level: 0 }) => {
      42.     this.setState(
      43.       {
      44.         ...{
      45.           introEnded: false,
      46.           loopStarted: false
      47.         },
      48.         ...settings
      49.       },
      50.       async () => {
      51.         const { data } = this.props;
      52.         const { chapter, level, introStarted } = this.state;
      53.         const { video } = data.chapters[chapter].levels[level];
      54. 
      55.         this.setState({
      56.           dataLoaded: false
      57.         });
      58. 
      59.         switch (video.type) {
      60.           case 'mi24':
      61.             await Promise.all([
      62.               VideoMI24Source(this.intro.current, video.intro),
      63.               VideoMI24Source(this.loop.current, video.loop)
      64.             ]);
      65.             break;
      66.           default:
      67.             this.intro.current.src = video.intro;
      68.             this.loop.current.src = video.loop;
      69.             break;
      70.         }
      71.         this.setState({
      72.           dataLoaded: true,
      73.           data: {
      74.             chapter: {
      75.               ...data.chapters[chapter],
      76.               next: data.chapters[chapter + 1]
      77.             },
      78.             level: {
      79.               ...data.chapters[chapter].levels[level],
      80.               next: data.chapters[chapter].levels[level + 1]
      81.             }
      82.           }
      83.         });
      84.         this.loop.current.addEventListener('canplay', this.dataLoaded, false);
      85. 
      86.         if (introStarted) {
      87.           this.intro.current.play();
      88.         }
      89.       }
      90.     );
      91.   };
      92. 
      93.   startLoop = () => {
      94.     // console.log('startLoop()');
      95.     this.setState({
      96.       loopStarted: true
      97.     });
      98.     this.loop.current.play();
      99.   };
      100. 
      101.   dataLoaded = e => {
      102.     const { loop } = this;
      103. 
      104.     this.setState({
      105.       aspecRatio: loop.current.offsetHeight / loop.current.offsetWidth
      106.     });
      107.     this.dragdrop.current.init();
      108.     this.loop.current.removeEventListener('canplay', this.dataLoaded, false);
      109.   };
      110. 
      111.   startIntro = () => {
      112.     // console.log('startIntro()');
      113.     this.setState({
      114.       introStarted: true
      115.     });
      116.     this.intro.current.play();
      117.   };
      118. 
      119.   introEnded = () => {
      120.     // console.log('introEnded()');
      121.     this.setState({
      122.       introEnded: true
      123.     });
      124.   };
      125. 
      126.   test = () =>
      127.     // console.log('MwocApp: test()');
      128.     true;
      129. 
      130.   render() {
      131.     const { introEnded, dataLoaded, loopStarted } = this.state;
      132. 
      133.     return (
      134.       <MwocAppContext.Provider value={this}>
      135.         <DragDrop ref={this.dragdrop}>
      136.           <video
      137.             className={classNames('o-mwoc-app__video', 'o-mwoc-app__video--loop')}
      138.             ref={this.loop}
      139.             playsInline
      140.             loop
      141.           />
      142.           <video
      143.             className={classNames('o-mwoc-app__video', 'o-mwoc-app__video--intro')}
      144.             hidden={introEnded !== false || dataLoaded !== true}
      145.             ref={this.intro}
      146.             playsInline
      147.             onEnded={this.introEnded}
      148.           />
      149.           <Hotspots />
      150.         </DragDrop>
      151.         <ControlPanel hidden={!loopStarted} />
      152.         <Overlay template="startIntro" />
      153.         <Overlay template="startLoop" />
      154.       </MwocAppContext.Provider>
      155.     );
      156.   }
      157. }
      158. 
      159. MwocApp.propTypes = {
      160.   data: PropTypes.shape({
      161.     chapters: PropTypes.arrayOf(
      162.       PropTypes.shape({
      163.         levels: PropTypes.arrayOf(
      164.           PropTypes.shape({
      165.             video: PropTypes.PropTypes.shape({
      166.               type: PropTypes.string.isRequired,
      167.               intro: PropTypes.string,
      168.               loop: PropTypes.string
      169.             }).isRequired,
      170.             hotspots: PropTypes.arrayOf(
      171.               PropTypes.shape({
      172.                 position: PropTypes.shape({
      173.                   left: PropTypes.string,
      174.                   right: PropTypes.string
      175.                 })
      176.               })
      177.             )
      178.           })
      179.         ).isRequired
      180.       })
      181.     ).isRequired
      182.   }).isRequired
      183. };
      184. 
      185. MwocApp.defaultProps = {};
      186. 
      
      

      Es-lint stört sich an den Zeilen 73-84 (react/no-unused-state). Die Regel habe ich grundsätzlich verstanden. Hier wird der state gesetzt, der in der Komponente garnicht benutzt wird. Allerdings verwende ich hier die Context-Api um in den Child-Komponenten auf den State zugreifen zu können.

      Was mache ich falsch?

      gruss, heinetz

      1. Hallo heinetz,

        // […]
        134.       <MwocAppContext.Provider value={this}>
        

        Es-lint stört sich an den Zeilen 73-84 (react/no-unused-state). Die Regel habe ich grundsätzlich verstanden. Hier wird der state gesetzt, der in der Komponente garnicht benutzt wird. Allerdings verwende ich hier die Context-Api um in den Child-Komponenten auf den State zugreifen zu können.

        Was mache ich falsch?

        Die Context-API ist nicht dafür gedacht, dass du das koplette Objekt durchreichst. Das führt das komplette Konzept der Isolierung durch Komponenten ad absurdum.

        Ich kann nicht mit Sicherheit sagen, was du hier falsch machst, da ich den Rest deines Codes nicht kenne. Aber <FooContext.Provider value={this} /> deutet ziemlich deutlich auf ein konzeptionelles Problem hin. Zumal this in deinem Fall ja auch Dinge enthält, die definitiv nicht an deine Kind-Komponenten weitergegeben werden sollten.

        Ohne den Code deiner anderen Komponenten zu kennen würde ich vermuten, dass du besser fährst, wenn du nur die Dinge aus deinem State weiter reichst, die du in den Kind-Komponenten auch benötigst. Oder wenn, wenn du wirklich den ganzen State benötigst, dass du auch nur den State weiter reichst (<FooContexit.provider value={this.state} />).

        LG,
        CK

        1. Hallo Ingrid,

          Ohne den Code deiner anderen Komponenten zu kennen würde ich vermuten, dass du besser fährst, wenn du nur die Dinge aus deinem State weiter reichst, die du in den Kind-Komponenten auch benötigst. Oder wenn, wenn du wirklich den ganzen State benötigst, dass du auch nur den State weiter reichst (<FooContexit.provider value={this.state} />).

          Ich denke, ich würde auch dann soweit gehen und den State explizit auseinander nehmen und ein neues Objekt bauen:

          const { chapter, level, data, dataLoaded, introStarted, introEnded, loopStarted, aspectRatio } = this.state;
          
          return (
            <FooContext.provider value={{chapter, level, data, dataLoaded, introStarted, introEnded, loopStarted, aspectRatio}}>
              ...
            </FooContext.provider>
          );
          

          So ist offensichtlich, was du übergibst und der Code bleibt insgesamt lesbarer. Außerdem schleicht sich nicht so schnell ein, dass du nach und nach immer mehr neue Felder hinzufügst, weil du jedesmal daran denken musst, dass du den Provider anpassen musst.

          Das alles natürlich nur, wenn ich beim aktuellen Konzept bleiben würde. Tatsächlich würde ich allerdings zunächst das Konzept noch einmal überdenken wollen…

          LG,
          CK

          1. Ok, dann habe ich scheinbar das Konzept nicht verstanden. Es ist mein zweites REACT-Projekt. Als meine Komponente grösser wurde hatte ich angefangen, Kind-Komponenten auszulagern. Dann kam ich an den Punkt, dass die übereinander Bescheid wissen und miteinander kommunizieren müssen. Dann bin ich auf die Context-Api gestossen, die mir genau diese Möglichkeit gegeben hat.

            Mit ...

            <MwocAppContext.Provider value={this}>

            ... habe ich in den Kindkomponenten sämtliche Informationen über die Elternkomponente zu Verfügung. Offenbar ist das problematisch und ich bin bereit, Deinem Ratschlag zu folgen, wenn ich verstanden habe, warum mein Weg nicht der richtige ist.

            Gruss, heinetz

            1. Hallo heinetz,

              Als meine Komponente grösser wurde hatte ich angefangen, Kind-Komponenten auszulagern. Dann kam ich an den Punkt, dass die übereinander Bescheid wissen und miteinander kommunizieren müssen.

              Ja, das ist ein typischer Verlauf 😀

              Dann bin ich auf die Context-Api gestossen, die mir genau diese Möglichkeit gegeben hat.

              Mit ...

              <MwocAppContext.Provider value={this}>

              ... habe ich in den Kindkomponenten sämtliche Informationen über die Elternkomponente zu Verfügung. Offenbar ist das problematisch und ich bin bereit, Deinem Ratschlag zu folgen, wenn ich verstanden habe, warum mein Weg nicht der richtige ist.

              Du hast mich missverstanden (oder ich habe mich missverständlich ausgedrückt). Ich habe nicht sagen wollen, dass dein Ansatz falsch ist – er funktioniert ja offensichtlich. Aber er hat Nachteile. Eine der Prinzipien von React ist, dass man seine Komponenten voneinander isoliert und so wenig wie möglich miteinander koppelt. Damit wird der Code einfacher bzw überhaupt erstmal testbar und die Komplexität wird reduziert.

              Der Gedanke ist, dass deine Komponenten einzelne „Inseln“ darstellen, mit klaren APIs (über die Proptypes), und nur über diese APIs kommuniziert wird. Dadurch kannst du innerhalb deiner Komponenten die Komplexität auf das Teilproblem reduzieren, die Abhängigkeiten untereinander schrumpfen und der Code wir übersichtlicher und testbar. Alles gute Dinge.

              Wenn du jetzt deine komplette Klasse (und nicht nur den State) über einen Kontext teilst, dann kannst du die Unterteilung in einzelne Komponenten auch fast lassen. Du stellst die größtmögliche Abhängigkeit her zwischen den einzelnen Komponenten, nix mehr mit loose coupling. Wenn ich etwas in einer Kind-Komponente ändern will, muss ich gleichzeitig die Eltern-Komponente verstehen und im Kopf haben sowie alle Geschwister-Komponenten. Und auch testen wird dadurch ziemlich schwierig: wie willst du die einzelnen Komponenten unabhängig voneinander testen?

              Um dein Problem zu lösen gibt es verschiedene Ansätze. Ein Ansatz wäre es Render-Props und Callback-Funktionen zu nutzen, die ein Ereignis kommunizieren und so den State deiner Eltern-Komponente ändern und mitteilen, z.B. sowas:

              import React, { useState } from "react";
              
              function CounterButton(props) {
                return <p><button onClick={ev => props.onClick()} /></p>;
              }
              
              function CounterDisplay(props) {
                return <p>{props.counter} mal geklickt.</p>;
              }
              
              function Parent(props) {
                const [counter, setCounter] = useState(0);
                return (
                  <div>
                    <CounterDisplay counter={counter} />
                    <CounterButton onClick={() => setCounter(counter + 1)} />
                  </div>
                );
              }
              

              In diesem (sehr simplen) Beispiel haben die beiden Kind-Komponenten keine Abhängigkeiten voneinander und sind sehr einfach testbar. Ihr Zweck und ihre API ist klar erkennbar.

              Ein anderer Weg wäre eine Flux-Architektur etwa mit Redux, bei dem es (fast) keinen lokalen State mehr gibt. Aber auch da gilt: je mehr du in die Stores der anderen Komponenten reingreifst, desto mehr Abhängigkeiten schaffst du zwischen den Komponenten.

              Vielleicht hilft dir auch Event-Bubbling.

              Ich kann dir leider nicht mehr sagen als diese allgemeinen Hinweise, da ich deine Aufgabe und deinen Code nicht kenne.

              LG,
              CK