reactjs - Mobx uppdateras inte med ändringar i array


Jag har problem med att få MobX att rendera när jag uppdaterar en array. Här är en förenklad version av koden:

import React, { useState } from 'react'
import { flow, makeObservable, observable } from 'mobx'
import { observer } from 'mobx-react'

import { ResourceList } from './ResourceList'
import { ResourceItem } from './ResourceItem'


export const View = observer(() => {
    const [{
        items,
    }] = useState<MyState>(new MyState())

    return <ResourceList items={items} />
})


export class MyState {
    constructor() {
        makeObservable(this)
        this._fetch()
    }

    @observable public items: ResourceItem[] = []

    private _fetch = flow(function* (this: MyState) {
        const items = yield fetchItems('myitems')
        this.items = items
    })
}

Som du kan se försöker jag använda arrayen i <ResourceList items={items} /> och det fungerar inte.

Det märkliga är att det fungerar om jag destruerar arrayen så här <ResourceList items={[...items]} />

fungerar också om jag bara gör console.log på en egenskap av arrayen:

console.log(items.length)
return <ResourceList items={items} />

Så det ser verkligen ut som att Mobx inte förstår att arrayen används i markupen.

Vad skulle vara det rätta sättet att fixa detta? Ska det inte finnas något jag konfigurerar inne i state-klassen för att få det att hända?

Tack

Svar #1

Detta diskuteras i delen Förstå reaktivitet i dokumentationen. När du skriver items avrefererar du inte en egenskap i items-arrayen, så din View-observatörskomponent spårar inte items-arrayen. Du kan slice arrayen eller sprida den som du gjorde i ditt exempel.

<ResourceList items={items.slice()} />

Du vill också skapa MyState-instansen en gång med hjälp av en funktion som ges till useState, så att du inte skapar en instans av klassen vid varje render som du inte använder, och det kommer att överbelasta nätverket.

const [{ items }] = useState<MyState>(() => new MyState());

Svar #2

Inte Mobx-problem

Problemet är inte ett mobx-problem. När du utlöser ändringen triggar datan en omrendering (det kan kontrolleras). Vad som inte omrenderas är ResourceList-komponenten. Den förväntar sig en ny referens. Eftersom myState.items är en observer och även myState. När du gör en tilldelning uppdateras samma observer-objekt. Istället för att ett nytt skapas. Vilket betyder samma referens. Genom omstrukturering eller att använda slice() skapas en ny array-referens och skickas ner via prop:en. Detta gör att ItemsList-komponenten omrenderas.

När kan det vara ett mobx-problem

För att mobx ska trigga en omrendering =>

två faktorer spelar in:

  • observerbar åtkomstoperation
  • Vilket observerbart och hur man utlöser en förändring för det

mobx spårar förändring genom åtkomst, inte värden, och renderar om genom åtgärder som gör förändring

ref: https://mobx.js.org/understanding-reactivity.html#mobx-tracks-property-access-not-values

Här har vi objekten som är observerbara av typen ObservableArray

Och MyState-objektet observerbara av typen ObservableDynamicObject

items är en del av myState. Och det spåras på myState observable. På grund av items egenskapstillgången som sker när omstruktureringen hände. Ingen åtkomst, inget spårning.

Och för att utlösa en förändring måste du göra en tilldelning till det objektet. myState.items=newRef. Att göra this.items=yield fetchItems('myitems') skulle göra det.

Så i princip, om du gör ändringen genom en tilldelning som ovan. Och tilldelningen av en ny referens (myState.items=myState.items) skulle inte trigga en omrendering.

Tilldelningen sker på myState ObservableDynamicObject-objektet och eftersom det är en proxy skulle dess set()-hanterare avlyssna det och hantera spårningsmagin.

// ...

let counter = 0;

export default observer(() => {
  const [{ items }] = useState(() => new MyState());
  counter++;
  // No items array access is made
  return <>{counter}</>;
});

class MyState {
  constructor() {
    makeAutoObservable(this, { autoBind: true });
    this.update();

    setTimeout(() => {
      this.update(); // Will render. Because an assignment on myState
             // happened for the items prop. And the access happened 
             // through the { items } destructuring
      setTimeout(() => {
        this.update(); // will re-render
        setTimeout(() => {
          this.update(); // will re-render
        }, 5000);
      }, 5000);
    }, 5000);
  }

  items = [];

  update = flow(function* () {
    const items = yield ["Mohamed", "Fahima", "The Magician"];
    // this.items = items
    this.items = [...this.items, items];
  });
}

Du kan kolla lekplatsen här

Array och mutationer

Nu, vad händer om förändringen snarare kommer genom this.items.push() myState.items.push(). I det här fallet är push-åtgärden, metoden, en egenskap hos myState.items-elementet som är av typen ObservableArray. Och det är också en proxy. När push anropas används Administration-objektets splice-hanterare. (push och splice hanteras av samma underliggande funktion). Vi bryr oss inte så mycket om det interna. Poängen är att push() på items kommer att utlösa förändring om det finns element som spåras på items-observablen och inte på myState. Och det skulle göra att åtkomst till element i komponentens renderingsfunktion blir ett måste för dess återrendering. Du gör det genom items[index], items.length. Och allt som items.map(), [...items] (destrukturering) skulle automatiskt göra indexåtkomst till arrayen (observerbar). Fångas av proxyens get()-hanterare. Och administrationsobjektet hanterar magin med att spåra ändringar i arrayen.

Så om din array inte renderas om eftersom du använde items.push(). Gör automatiskt en åtkomst till items-arrayen, som items.length bara för att testa. Och du måste alltid använda en array om du spårar den för förändring. Så antingen items.map() eller [...items] eller items.slice() eller något som gör en index access. Eller åtkomst till items.length.

// ...

let counter = 0;

export default observer(() => {
  const [{ items }] = useState(() => new MyState());
  counter++;
  // No access to items array was made
  return <>{counter}</>;
});

class MyState {
  constructor() {
    makeAutoObservable(this, { autoBind: true });
    this.update();

    setTimeout(() => {
      this.update(); // No re-rendering (nothing tracked) (no access made)
      setTimeout(() => {
        this.update(); // No re-rendering
        setTimeout(() => {
          this.update(); // No re-rendering
        }, 5000);
      }, 5000);
    }, 5000);
  }

  items = [];

  update = flow(function* () {
    const items = yield ["Mohamed", "Fahima", "The Magician"];
    this.items.push(...items);
  });
}

Detta exempel kommer att stoppa räknaren vid 4 i ett strict mode allt körs två gånger. så 2 första renderingar och 2 för omrendering på grund av mobx observer högre ordningens komponent som skulle utlösa omrendering en gång (2 strict-mode).

Du kan kolla lekplatsen här

Och om vi bara lägger till något av items.slice() eller items.length eller items[0] eller [...items] eller items.map(() => true) så kommer det att renderas om korrekt vid update() med push.

let counter = 0;

export default observer(() => {
  const [{ items }] = useState(() => new MyState());
  counter++;
  // items[0];
  // items.length;
  // items.slice()
  items.map(() => true);
  return <>{counter}</>;
});

class MyState {
  constructor() {
    makeAutoObservable(this, { autoBind: true });
    this.update();

    setTimeout(() => {
      this.update(); // Will re-render (array observable is tracked due to the access)
      setTimeout(() => {
        this.update(); // will re-render
        setTimeout(() => {
          this.update(); // will re-render
        }, 5000);
      }, 5000);
    }, 5000);
  }

  items = [];

  update = flow(function* () {
    const items = yield ["Mohamed", "Fahima", "The Magician"];
    this.items.push(...items);
  });
}

Och här är lekplatsen igen. För denna kommer räknaren att nå 10. Omrendering sker varje gång när update() körs genom den kedjade setTimeout

För mer information kan du kolla mitt svar här: https://stackoverflow.com/a/73564630/7668448

Relaterade artiklar