带React Native钩子的上下文在异步函数上返回空对象/数组

时间:2020-10-16 14:45:51

标签: reactjs react-native async-await react-hooks react-context

我已经在这个问题上进行了一段时间的战斗,但我似乎不知道该如何解决。

在主屏幕上,我获取用户坐标和一系列电影院。 Cinemas数组包含每个电影院的坐标,我将其与用户坐标一起用于计算从用户到电影院的距离。

在CinemaContext中获取和存储电影院是可行的,但是一旦我运行了计算距离的函数,上下文中的电影院对象就为空。 距离计算会向数组中的每个电影院添加一个距离值,然后返回新的数组。

奇怪的是,当我尝试时,电影上下文对象为空。然后,如果我在CinemaContext或getUserCoordinates函数内部进行编辑,然后导航到电影院概览屏幕,那么电影院就在那儿并带有距离值。

必须具有加载顺序或异步功能,因为代码是“有效的”,但似乎并没有在正确的时间填充上下文或用空值覆盖上下文。

我应该补充一点,我在另一个屏幕上使用Cinemas数组,在这里可以像这样访问它:

const { state } = useContext(Context)

Home.js

import React, { useState, useEffect, useContext } from "react";
import { View, Text, StyleSheet, TouchableOpacity, StatusBar, ActivityIndicator } from "react-native";
import * as Location from 'expo-location';
import { Context } from "../context/CinemaContext";

const Home = ({ navigation }) => {

  const [errorMsg, setErrorMsg] = useState(null);
  const { state, updateCinemas, getCinemas } = useContext(Context)

  // Fetch user coordinates and call updateCinemas with the coordinates and cinemas 
  const getUserCoordinates = async (cinemas) => {
    try {
      const granted = await Location.requestPermissionsAsync();
      
      if (granted) {
        let location = await Location.getCurrentPositionAsync({});

        await updateCinemas(cinemas, location.coords.latitude, location.coords.longitude)
        
      } else {
        throw new Error("Location permission not granted");
      }
    } catch (e) {
      setErrorMsg(e)
    }
  }

  useEffect(() => {
    if (state.cinemas.length === 0) {
      getCinemas();
    }
    getUserCoordinates(state.cinemas);
  }, []);

  if (!state.cinemas) {
    return <ActivityIndicator size="large" style={{ marginTop: 200 }} />
  }

  return ( Some views ..)

CinemaContext.js

import dataContext from "./DataContext";
import _ from "lodash";
import { computeDistance } from "../helpers/utils";


const cinemaReducer = (state, action) => {
  switch (action.type) {
    case "add_error":
      return { ...state, errorMessage: action.payload };
    case "get_cinemas":
      return { ...state, cinemas: action.payload };
    case "update_cinemas":
      return { ...state, cinemas: action.payload };
    default:
      return state
  }
};

const getCinemas = dispatch => async () => {
  try {
    const response = await fetch(
      "url-to-cinemas-array",
      { mode: "no-cors" })
    const cinemas = await response.json();
    dispatch({
      type: "get_cinemas",
      payload: cinemas
    });
  } catch (err) {
    dispatch({
      type: "add_error",
      payload: "Something went wrong with the cinemas"
    })
  }
}

const updateCinemas = (dispatch) => {

  return async (cinemas, referenceLat, referenceLong) => {
    
    const cinemasWithDistance = cinemas.map(cinema => {
      return {
        ...cinema,
        distance: computeDistance([cinema.geo.latitude, cinema.geo.longitude], [referenceLat, referenceLong]) // Calculate the distance
        
      };
    });    
    const orderedCinemas = _.orderBy(cinemasWithDistance, 'distance');
    dispatch({ type: "update_cinemas", payload: orderedCinemas });
  }
}

export const { Context, Provider } = dataContext(
  cinemaReducer,
  { updateCinemas, getCinemas },
  { cinemas: [], errorMessage: '' }
);

DataContext.js

import React, { useReducer } from 'react';

export default (reducer, actions, defaultValue) => {
  const Context = React.createContext();

  const Provider = ({ children }) => {
    const [state, dispatch] = useReducer(reducer, defaultValue);

    const boundActions = {};
    for (let key in actions) {
      boundActions[key] = actions[key](dispatch);
    }

    return (
      <Context.Provider value={{ state, ...boundActions }}>
        {children}
      </Context.Provider>
    );
  };

  return { Context, Provider };
};

App.js

import React from "react";
import RootStackNavigator from "./src/navigation/RootStackNavigator";
import { Provider } from "./src/context/CinemaContext";

export default function App() {

  return (
    <Provider>
      <RootStackNavigator />
    </Provider>
  );
};

任何帮助将不胜感激! 预先感谢!

1 个答案:

答案 0 :(得分:0)

我认为,当您调用class Backgrounds extends StatefulWidget { @override _BackgroundsState createState() => _BackgroundsState(); } class _BackgroundsState extends State<Backgrounds> { List<String> imagePath = [ 'images/image1.png', 'images/image2.png', 'images/image3.png', 'images/image4.png', 'images/image5.png', 'images/image6.png', 'images/image7.png', 'images/image8.png', 'images/image9.png' ]; @override Widget build(BuildContext context) { return GridView.count( scrollDirection: Axis.vertical, crossAxisCount: 3, children: List.generate( 9, (index) => index == 0 ? ToggleButtonWidget( isFirst: true, imagePath: imagePath[index], ) : ToggleButtonWidget( imagePath: imagePath[index], ), ), ); } } class ToggleButtonWidget extends StatefulWidget { final bool isFirst; final String imagePath; ToggleButtonWidget({this.isFirst = false, this.imagePath}); @override _ToggleButtonWidgetState createState() => _ToggleButtonWidgetState(); } class _ToggleButtonWidgetState extends State<ToggleButtonWidget> { List<bool> _isSelected; @override void initState() { _isSelected = [widget.isFirst ? true : false]; super.initState(); } @override Widget build(BuildContext context) { return Container( height: 100, width: 107, child: ToggleButtons( children: [ Image.asset(widget.imagePath), ], isSelected: _isSelected, onPressed: (int index) { setState(() { _isSelected[0] = !_isSelected[0]; }); }, selectedBorderColor: Color(0xff2244C7), borderWidth: 3, borderRadius: BorderRadius.all(Radius.circular(8)), ), ); } } state.cinemas仍然为空,因此getUserCoordinates(state.cinemas);动作将以一个空数组运行,覆盖"update_cinemas"之前更新的电影院数组。您可以通过在"get_cinemas"调用之前添加console.log('state.cinemas.length: ', state.cinemas.length);来验证这一点。

对此,我认为一种解决方案是在依赖于getUserCoordinates数组的单独getUserCoordinates中调用useEffect(这样每次state.cinemas更改时它都会再次运行) :

state.cinemas