Javascript-форум (https://javascript.ru/forum/)
-   Библиотеки/Тулкиты/Фреймворки (https://javascript.ru/forum/library-toolkit-framework/)
-   -   [React Native] MobX + FlatList кто использует такую связку? (https://javascript.ru/forum/library-toolkit-framework/82841-%5Breact-native%5D-mobx-flatlist-kto-ispolzuet-takuyu-svyazku.html)

ksa 15.07.2021 17:08

[React Native] MobX + FlatList кто использует такую связку?
 
Пишем на React Native мобильное приложение... Состояние (массив объектов) хранится в MobX...
Решили использовать для отображения массива элементов компонент FlatList... Он все отображает но получаем в консоли "предупреждение"
[mobx] Out of bounds read: 25

Загуглили что такое значит... :)
Де попытка выводить элементы без отслеживания.

Делали клон этого массива, хотели потом вывести клон. Дает ошибку, де включен строгий режим и вы пытаетесь менять данные из MobX...

Есть у кого решение такой проблемы?
Может кто имел с этим дело?
Как поборол?

Alexandroppolus 15.07.2021 17:27

Можешь накидать простенький пример, где воспроизводится?

"попытка выводить элементы без отслеживания" - не забыл навесить observer на компонент?

ksa 15.07.2021 18:33

Цитата:

Сообщение от Alexandroppolus
не забыл навесить observer на компонент?

На сам-то компонет observer навешен... А вот FlatList, в который передается массив, не оборачивается. :(

Цитата:

Сообщение от Alexandroppolus
Можешь накидать простенький пример, где воспроизводится?

Завтра попрошу ребят подготовить простенький пример для демонстрации. :yes:

Aetae 15.07.2021 19:32

Ну дак. Перед тем как передавать массив в FlatList или куда ещё, что тобой не контролируется - делай предварительно arr.slice()/toJS(arr).
Потому что массив в mobx - НЕ массив(arr instanceof Array // false), а массивоподобный mobx-объект. Почему они не взяли за основу обычный массив, как в Vue? А чтобы все страдали.

ksa 15.07.2021 22:53

Цитата:

Сообщение от Aetae
делай предварительно
arr.slice()/toJS(arr)
.

Все делали... :D
Пишет, де включен строгий режим, вы пытаетесь менять отслеживаемые данные.
Цитата:

Сообщение от Aetae
Потому что массив в mobx - НЕ массив

Мы это знаем. :yes:

ksa 15.07.2021 22:55

Завтра покажу все наши "ипостаси" - посмотрите, может чего не так делали...
И тексты ошибок приведу полностью.

Aetae 15.07.2021 23:50

Цитата:

Сообщение от ksa (Сообщение 538693)
Пишет, де включен строгий режим, вы пытаетесь менять отслеживаемые данные.

По идее после .slice() должен получиться обычный массив, не имеющий отношения к mobx. Проблема где-то в другом месте, значит.

Alexandroppolus 16.07.2021 00:27

Цитата:

Сообщение от Aetae
По идее после .slice() должен получиться обычный массив, не имеющий отношения к mobx.

если там массив объектов, который @observable (то есть deep), то slice() вернёт массив отслеживаемых объектов, это ведь поверхностная копия.

Aetae 16.07.2021 01:26

Alexandroppolus, ох как же это больно. Больнее мне только от сраных рефов в Vue 3.

ksa 16.07.2021 07:42

Цитата:

Сообщение от Aetae
По идее после .slice() должен получиться обычный массив, не имеющий отношения к mobx.

Так и получается, мы в консоль выводим результат. Но именно после этих действий "попытка поменять" и получается, указывает именно на эту строчку...

ksa 16.07.2021 07:44

Цитата:

Сообщение от Alexandroppolus
если там массив объектов, который @observable (то есть deep), то slice() вернёт массив отслеживаемых объектов, это ведь поверхностная копия.

Мы и по "массиву" проходились фором, каждый элемент делали обычным объектом и вставляли в новый массив.
В консоли все нормально, но тут же ошибка про попытку менять отслеживаемые данные...
Скоро поеду на работу - сделаем примеры, покажу тут.

ksa 16.07.2021 09:45

Ситуация получается более запутанной...
Сделал я тестик простенький и у меня все заработало без всяких предупреждений!

Хранилище
import {makeAutoObservable} from 'mobx'

class Test {
	data = {
		arr: [
		    {id: '0', name: 'Item 0'},
		    {id: '1', name: 'Item 1'},
		    {id: '2', name: 'Item 2'},
		]
	}
	constructor(props) {
		makeAutoObservable(this)
	}
	add(obj) {
	    this.data.arr.push(obj)
	}
}

export default new Test()

Собственно экран
import React from "react"
import { StyleSheet, Text, View, Button, FlatList } from 'react-native'
import { observer} from 'mobx-react-lite'
import { toJS } from "mobx"

import test from '../store/test'

function Test() {
    //console.log(1, test.data.arr)
	const arr = toJS(test.data.arr)
    //console.log(2, test.data.arr)

	const renderItem = ({ item }) => (
	    <Text>{item.name}</Text>
	)

	return (
	    <View style={styles.container}>
    	    <View style={styles.container}>
    	        <Button
    	            title='Add'
    	            onPress={add}
    	        />
    	    </View>
            <FlatList
                data={test.data.arr}
                renderItem={renderItem}
                keyExtractor={item => item.id}
            />
	    </View>
	)
    function add() {
        const l = test.data.arr.length
        test.add({
            id: l.toString(),
            name: 'Item ' + l
        })
    }
}

const styles = StyleSheet.create({
	container: {
		flex: 1,
		backgroundColor: '#fff',
		alignItems: 'center',
		justifyContent: 'center',
	},
	tab: {
	    margin: 50
	}
})

export default observer(Test)

Все рисуется и так нормально... НО! :)
если убрать
const arr = toJS(test.data.arr)

Перестает работать добавление элементов. :blink:
Возвращаю эту строку - работает добавление.

А по предыдущей проблеме - сказал чтобы ребята начали искать у себя проблемы глубже...

ksa 16.07.2021 10:14

Цитата:

Сообщение от ksa
сказал чтобы ребята начали искать у себя проблемы глубже...

Выяснили следующее...
Было асинхронное чтение данных с сервера. Изменение состояния обернули в runInAction() из mobx и ошибка ушла. FlatList стал отображать данные из хранилища mobx без замечаний.

Но по моему примеру все еще интересно что так влияет на работоспособность кнопки Add в моем примере? :-?

Alexandroppolus 16.07.2021 10:45

Цитата:

Сообщение от ksa
Но по моему примеру все еще интересно что так влияет на работоспособность кнопки Add в моем примере?

кнопка Add работает, просто компонент не перерисовывается :)

без toJS ты читаешь только test.data.arr (и как следствие подписываешься только на изменение test.data.arr), а он не меняется, массив тот же самый, только в него запушили.

вызов toJS(test.data.arr) делает чтение вообще всего, что есть внутри массива, и соответственно подписки будут на все изменения.

и конечно, здесь не самая экономичная схема перерендеров. Что-то поменяли, и перерендеривается всё. Тут надо тоже смотреть по ситуации, как лучше сделать, и разумеется, без toJS

Alexandroppolus 16.07.2021 10:47

Цитата:

Сообщение от ksa
Изменение состояния обернули в runInAction() из mobx и ошибка ушла.

а в тестовом примере изменения стейта были внутри сторовского метода add, который по воле makeAutoObservable обернулся в action и получил право менять данные.

ksa 16.07.2021 11:58

Цитата:

Сообщение от Alexandroppolus
просто компонент не перерисовывается

Вона че...

Цитата:

Сообщение от Alexandroppolus
в тестовом примере изменения стейта были внутри сторовского метода add, который по воле makeAutoObservable обернулся в action и получил право менять данные

Так вот я и думал что это даст возможность рендеренга FlatList... А оно видал как. :(
Цитата:

Сообщение от Alexandroppolus
Тут надо тоже смотреть по ситуации, как лучше сделать, и разумеется, без toJS

И что можно придумать "альтернативного"? :-?
В "простом" Реакте вполне хватало использования "сторных" методов и все перерисовывалось...
А теперь этого мало... :(

Aetae 16.07.2021 12:34

А потому, что он цепляется через жёпу. Потому что реакт - не расширяем нихрена. Вызовы рендера: стейт - отдельно, пропс - отдельно, нужен стор? - тоже отдельно.

Можешь попробовать энтот твой FlatList в <Observer><FlatList ...></Observer> заключить, но это так - выстрел в небо, загружать сейчас в голову весь контекст как оно там на самом деле работает откровенно лень.)

Alexandroppolus 16.07.2021 13:04

Цитата:

Сообщение от Aetae
Вызовы рендера: стейт - отдельно, пропс - отдельно, нужен стор? - тоже отдельно.

но ведь сам рендер вызывается однократно, если в коде синхронно поменялось и то и другое и третье, так что не проблема
Цитата:

Сообщение от Aetae
Можешь попробовать энтот твой FlatList в <Observer><FlatList ...></Observer> заключить

не поможет, FlatList от этого не станет обзервером

я бы попробовал так:
const MobxFlatList = observer(FlatList)

и потом юзать MobxFlatList. А уж если такое не поможет - менять массив иммутабельно, то есть вместо test.data.arr.push(newItem) делать test.data.arr = [...test.data.arr, newItem]
ну и там далее ещё кое-где поменять.

использовать в рендере toJS - это антиMobX

ksa 16.07.2021 13:38

Цитата:

Сообщение от Aetae
Можешь попробовать энтот твой FlatList в
<Observer><FlatList ...></Observer>
заключить

Это первое что мы опробовали... :D
Не помогает.

Цитата:

Сообщение от Alexandroppolus
я бы попробовал так:
const MobxFlatList = observer(FlatList)

Ща спробанем!

Цитата:

Сообщение от Alexandroppolus
А уж если такое не поможет - менять массив иммутабельно, то есть вместо test.data.arr.push(newItem) делать
test.data.arr = [...test.data.arr, newItem]

Опробую и это...

Цитата:

Сообщение от Alexandroppolus
использовать в рендере toJS - это антиMobX

Это-то понятно...

ksa 16.07.2021 13:44

Цитата:

Сообщение от Alexandroppolus
я бы попробовал так:
const MobxFlatList = observer(FlatList)

Сделал так...

import React from "react"
import { StyleSheet, Text, View, Button, FlatList } from 'react-native'
import { observer} from 'mobx-react-lite'
import { toJS, runInAction } from "mobx"

import test from '../store/test'

function Test() {
	//const arr = toJS(test.data.arr)

	const renderItem = ({ item }) => (
	    <Text>{item.name}</Text>
	)

	const MobxFlatList = observer(FlatList)

	return (
	    <View style={styles.container}>
    	    <View style={styles.tab}>
    	        <Button
    	            title='Add'
    	            onPress={add}
    	        />
    	    </View>
            <MobxFlatList
                data={test.data.arr}
                renderItem={renderItem}
                keyExtractor={item => item.id}
            />
	    </View>
	)
    function add() {
        const l = test.data.arr.length
        test.add({
            id: l.toString(),
            name: 'Item ' + l
        })
    }
}

const styles = StyleSheet.create({
	container: {
		flex: 1,
		color: '#000',
		backgroundColor: '#fff',
		alignItems: 'center',
		justifyContent: 'center',
	},
	tab: {
	    margin: 70
	}
})

export default observer(Test)

Ошибка
Cannot call a class as a function

Aetae 16.07.2021 13:47

Цитата:

Сообщение от Alexandroppolus (Сообщение 538720)
но ведь сам рендер вызывается однократно, если в коде синхронно поменялось и то и другое и третье, так что не проблема

Ну так ващет нет. Три изменения - три рендера и будет. Если не больше.

ksa 16.07.2021 13:49

Цитата:

Сообщение от Alexandroppolus
А уж если такое не поможет - менять массив иммутабельно, то есть вместо test.data.arr.push(newItem) делать
test.data.arr = [...test.data.arr, newItem]

А вот такое сработало!

import {makeAutoObservable} from 'mobx'

class Test {
	data = {
		arr: [
		    {id: '0', name: 'Item 0'},
		    {id: '1', name: 'Item 1'},
		    {id: '2', name: 'Item 2'},
		]
	}
	constructor(props) {
		makeAutoObservable(this)
	}
	add(obj) {
	    //this.data.arr.push(obj)
	    this.data.arr = [...this.data.arr, obj]
	}
}

export default new Test()


С этим работает и так. :victory:
import React from "react"
import { StyleSheet, Text, View, Button, FlatList } from 'react-native'
import { observer} from 'mobx-react-lite'
import { toJS, runInAction } from "mobx"

import test from '../store/test'

function Test() {
	const renderItem = ({ item }) => (
	    <Text>{item.name}</Text>
	)

	return (
	    <View style={styles.container}>
    	    <View style={styles.tab}>
    	        <Button
    	            title='Add'
    	            onPress={add}
    	        />
    	    </View>
            <FlatList
                data={test.data.arr}
                renderItem={renderItem}
                keyExtractor={item => item.id}
            />
	    </View>
	)
    function add() {
        const l = test.data.arr.length
        test.add({
            id: l.toString(),
            name: 'Item ' + l
        })
    }
}

const styles = StyleSheet.create({
	container: {
		flex: 1,
		color: '#000',
		backgroundColor: '#fff',
		alignItems: 'center',
		justifyContent: 'center',
	},
	tab: {
	    margin: 70
	}
})

export default observer(Test)

Alexandroppolus 16.07.2021 14:26

Цитата:

Сообщение от Aetae
Ну так ващет нет. Три изменения - три рендера и будет. Если не больше.

вот накидал пример.
по кнопке случается все 3 вида изменений - две замены стейта, значение в пропсе, наблюдаемое значение в сторе. Перерендер один - он, похоже, идет следующим микротаском, потому что если store.func() обернуть в микротаск, то уже будет 2 рендера.

в общем, реакт с мобиксом не так уж плохи :)

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

class Store {
  constructor() {
    makeObservable(this)
  }

  @observable a = 1

  @observable b = 2

  @action func() {
    this.a += 1
    this.b += 1
  }
}

const store = new Store()

const Comp: React.FC<{ a: number }> = observer(({ a }) => {
  const [c, setC] = useState(3)
  const [d, setD] = useState(4)
  console.log('*** render Comp ', a, store.b, c, d)

  const handler = () => {
    setC(c + 1)
    setD(d + 1)
    // Promise.resolve().then(() => store.func())
    store.func()
  }

  return (
    <div>
      <div>{`a = ${a}, store.b = ${store.b}, c = ${c}, d = ${d}`}</div>
      <button type="button" onClick={handler}>
        change
      </button>
    </div>
  )
})

export const App: React.FC = observer(() => {
  console.log('*** render App')
  return <Comp a={store.a} />
})

Alexandroppolus 16.07.2021 14:33

Цитата:

Сообщение от ksa
...код.....

Cannot call a class as a function

попробуй заимпортировать так:
import { observer} from 'mobx-react'

ещё поправки, для чистоты эксперимента:

const MobxFlatList = observer(FlatList)
это вынеси наружу из function Test()

const renderItem = ...
тоже вынеси

функцию для keyExtractor тоже вынеси отдельно наружу

ksa 16.07.2021 14:57

Alexandroppolus, я все это "запишу себе в книжечка"... (с)
Мало ли когда пригодится. :)

Больше устроил вариант с другим подходом собственно изменения массива.


Часовой пояс GMT +3, время: 17:04.