React钩子将useState元素记录为null(如果不是)

时间:2019-09-01 10:47:57

标签: reactjs react-hooks

我有一种方法,

const handleUpvote = (post, index) => {
  let newPosts = JSON.parse(JSON.stringify(mappedPosts));

  console.log('mappedPosts', mappedPosts); // null
  console.log('newPosts', newPosts); // null

  if (post.userAction === "like") {
    newPosts.userAction = null;
  } else {
    newPosts.userAction = "like";
  }

  setMappedPosts(newPosts);

  upvote(user.id, post._id);
};

该元素附加到映射的元素上,

const mapped = userPosts.map((post, index) => ( 
    <ListItem 
      rightIcon = {
        onPress = {
          () => handleUpvote(post, index)
        }
   ......

我有

  const [mappedPosts, setMappedPosts] = useState(null);

安装组件时,它将userPosts从redux状态中取出,将它们映射到ListItem并适当地显示它。问题在于,每当输入handleUpvote()时,它将mappedPosts视为空,因此在setMappedPosts(newPosts);

处将整个列表设置为空。

我在这里做错了什么? mappedPosts在单击handleUpvote()时确实不为null,因为如果第一个调用mappedPosts方法的元素是handleUpvote(),那怎么可能呢?地方?

我尝试过类似的

    setMappedPosts({
      ...mappedPosts,
      mappedPosts[index]: post
    });

但是那甚至不能编译。不确定从这里去哪里


编辑

整个组件:

const Profile = ({
  navigation,
  posts: { userPosts, loading },
  auth: { user, isAuthenticated },
  fetchMedia,
  checkAuth,
  upvote,
  downvote
}) => {
  const { navigate, replace, popToTop } = navigation;

  const [mappedPosts, setMappedPosts] = useState(null);

  useEffect(() => {
    if (userPosts) {
      userPosts.forEach((post, index) => {
        post.userAction = null;

        post.likes.forEach(like => {
          if (like._id.toString() === user.id) {
            post.userAction = "liked";
          }
        });

        post.dislikes.forEach(dislike => {
          if (dislike._id.toString() === user.id) {
            post.userAction = "disliked";
          }
        });
      });

      const mapped = userPosts.map((post, index) => (
        <ListItem
          Component={TouchableScale}
          friction={100}
          tension={100}
          activeScale={0.95}
          key={index}
          title={post.title}
          bottomDivider={true}
          rightIcon={
            <View>
              <View style={{ flexDirection: "row", justifyContent: "center" }}>
                <Icon
                  name="md-arrow-up"
                  type="ionicon"
                  color={post.userAction === "liked" ? "#a45151" : "#517fa4"}
                  onPress={() => handleUpvote(post, index)}
                />
                <View style={{ marginLeft: 10, marginRight: 10 }}>
                  <Text>{post.likes.length - post.dislikes.length}</Text>
                </View>
                <Icon
                  name="md-arrow-down"
                  type="ionicon"
                  color={post.userAction === "disliked" ? "#8751a4" : "#517fa4"}
                  onPress={() => handleDownvote(post, index)}
                />
              </View>
              <View style={{ flexDirection: "row" }}>
                <Text>{post.comments.length} comments</Text>
              </View>
            </View>
          }
          leftIcon={
            <View style={{ height: 50, width: 50 }}>
              <ImagePlaceholder
                src={post.image.location}
                placeholder={post.image.location}
                duration={1000}
                showActivityIndicator={true}
                activityIndicatorProps={{
                  size: "large",
                  color: index % 2 === 0 ? "blue" : "red"
                }}
              />
            </View>
          }
        ></ListItem>
      ));

      setMappedPosts(mapped);
    } else {
      checkAuth();
      fetchMedia();
    }
  }, [userPosts, mappedPosts]);

  const handleDownvote = (post, index) => {
    let newPosts = JSON.parse(JSON.stringify(mappedPosts));

    if (post.userAction === "dislike") {
      newPosts.userAction = null;
    } else {
      newPosts.userAction = "dislike";
    }

    setMappedPosts(newPosts);

    downvote(user.id, post._id);
  };

  const handleUpvote = post => {
    let newPosts = JSON.parse(JSON.stringify(mappedPosts));

    console.log("mappedPosts", mappedPosts); // null
    console.log("newPosts", newPosts); // null

    if (post.userAction === "like") {
      newPosts.userAction = null;
    } else {
      newPosts.userAction = "like";
    }

    setMappedPosts(newPosts);

    upvote(user.id, post._id);
  };

  return mappedPosts === null ? (
    <Spinner />
  ) : (
    <ScrollView
      refreshControl={
        <RefreshControl
          refreshing={false}
          onRefresh={() => {
            this.refreshing = true;
            fetchMedia();
            this.refreshing = false;
          }}
        />
      }
    >
      {mappedPosts}
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center"
  }
});

Profile.propTypes = {
  auth: PropTypes.object.isRequired,
  posts: PropTypes.object.isRequired,
  fetchMedia: PropTypes.func.isRequired,
  checkAuth: PropTypes.func.isRequired,
  upvote: PropTypes.func.isRequired,
  downvote: PropTypes.func.isRequired
};

const mapStateToProps = state => ({
  auth: state.auth,
  posts: state.posts
});

export default connect(
  mapStateToProps,
  { fetchMedia, checkAuth, upvote, downvote }
)(Profile);

2 个答案:

答案 0 :(得分:1)

当前解决方案不起作用的原因是,您正在userPosts钩子内部渲染useEffect,看起来像它只运行一次,最终“缓存”了初始状态,这就是您最终在处理程序中看到的内容。

您将需要使用多个钩子才能使其正常工作:

const Profile = (props) => {
  // ...
  const [mappedPosts, setMappedPosts] = useState(null)
  const [renderedPosts, setRenderedPosts] = useState(null)

  useEffect(() => {
    if (props.userPosts) {
      const userPosts = props.userPosts.map(post => {
        post.userAction = null;

        // ...
      })      
      setMappedPosts(userPosts)
    } else {
      checkAuth()
      fetchMedia()
    }
  }, [props.userPosts])

  const handleDownvote = (post, index) => {
    // ...
    setMappedPosts(newPosts)
  }
  const handleUpvote = (post) => {
    // ...
    setMappedPosts(newPosts)
  }

  useEffect(() => {
    if (!mappedPosts) {
      return
    }
    const renderedPosts = mappedPosts.map((post, index) => {
      return (...)
    })
    setRenderedPosts(renderedPosts)
  }, [mappedPosts])

  return !renderedPosts ? null : (...)
}

这是一个简化的示例,可以完成您要执行的操作:


另外,请注意,请勿这样做

const Profile = (props) => {
  const [mappedPosts, setMappedPosts] = useState(null)

  useEffect(() => {
    if (userPosts) {
      setMappedPosts() // !!!!
    } else {
      // ...
    }
  }, [userPosts, mappedPosts])
}

不要在依赖项数组中更新具有它的钩子内部的状态。您将陷入无限循环,这将导致您的组件继续重新渲染,直到崩溃。

答案 1 :(得分:0)

让我用一个简化的例子来说明问题:

const Example = props => {
    const { components_raw } = props;
    const [components, setComponents] = useState([]);
    const logComponents = () => console.log(components);

    useEffect(() => {
        // At this point logComponents is equivalent to
        // logComponents = () => console.log([])
        const components_new = components_raw.map(_ => (
            <div onClick={logComponents} />
        ));

        setComponents(components_new);
    }, [components_raw]);

    return components;
};

您可以看到调用setComponents的周期,组件为空[]。分配状态后,状态将保持为logComponents的值,在以后的周期中是否更改都无关紧要。

要解决此问题,您可以从接收到的数据中修改必要的元素,无需任何组件。然后在渲染的返回值上添加onClick。

const Example = props => {
    const { data_raw } = props;
    const [data, setData] = useState([]);
    const logData = () => console.log(data);

    useEffect(() => {
        const data_new = data_raw.map(data_el => ({
            ...data_el // Any transformation you need to do to the raw data.
        }));

        setData(data_new);
    }, [data_raw]);

    return data.map(data_el => <div {...data_el} onClick={logData} />);
};