Родственный компонент не выполняет повторную визуализацию при изменении состояния (используя useEffect, useState и Context)

#javascript #reactjs #react-hooks #use-effect #use-state

#javascript #reactjs #реагирующие крючки #использование-эффект #состояние использования

Вопрос:

В моем Main.js Я создаю первое глобальное состояние с именем пользователя и списком пользователей, за которыми я слежу.

Затем как компонент Wall, так и FollowingSidebar отображают список подписчиков и их сообщения (плюс сообщения основного пользователя).

Пока все хорошо. Но во вложенном компоненте внутри FollowingSidebar, называемом FollowingUser, у меня есть onClick для удаления пользователя. Я понимаю, что, поскольку я изменяю состояние, useEffect позаботится о компоненте стены, чтобы повторно отобразить его, но ничего не происходит… Я проверил несколько примеров в Интернете, но пока ничто не помогло моему варианту использования.

Излишне говорить, что я не слишком разбираюсь в React, и хуки немного сложны.

Код здесь:

Main.js:

 import React, { useEffect, useState } from "react";
import ReactDom from "react-dom";
import db from "./firebase.js";

// Components
import Header from "./components/Header";
import FollowingSidebar from "./components/FollowingSidebar";
import SearchUsers from "./components/SearchUsers";
import NewMessageTextarea from "./components/NewMessageTextarea";
import Wall from "./components/Wall";

// Context
import StateContext from "./StateContext";

function Main() {
  const [mainUser] = useState("uid_MainUser");
  const [follows, setFollows] = useState([]);

  const setInitialFollows = async () => {
    let tempFollows = [mainUser];
    const user = await db.collection("users").doc(mainUser).get();
    user.data().following.forEach(follow => {
      tempFollows.push(follow);
    });
    setFollows(tempFollows);
  };

  useEffect(() => {
    setInitialFollows();
  }, []);

  const globalValues = {
    mainUserId: mainUser,
    followingUsers: follows
  };

  return (
    <StateContext.Provider value={globalValues}>
      <Header />
      <FollowingSidebar />
      <SearchUsers />
      <NewMessageTextarea />
      <Wall />
    </StateContext.Provider>
  );
}

ReactDom.render(<Main />, document.getElementById("app"));

if (module.hot) {
  module.hot.accept();
}
 

Компонент FollowingSidebar:

 import React, { useState, useEffect, useContext } from "react";
import db from "../firebase.js";
import StateContext from "../StateContext";
import FollowingUser from "./FollowingUser";

export default function FollowingSidebar() {
  const { followingUsers } = useContext(StateContext);
  const [users, setUsers] = useState(followingUsers);

  useEffect(() => {
    const readyToRender = Object.values(followingUsers).length > 0;
    if (readyToRender) {
      db.collection("users")
        .where("uid", "in", followingUsers)
        .get()
        .then(users => {
          setUsers(users.docs.map(user => user.data()));
        });
    }
  }, [followingUsers]);

  return (
    <section id="following">
      <div className="window">
        <h1 className="window__title">People you follow</h1>
        <div className="window__content">
          {users.map((user, index) => (
            <FollowingUser avatar={user.avatar} username={user.username} uid={user.uid} key={index} />
          ))}
        </div>
      </div>
    </section>
  );
}
 

Компонент FollowingUser:

 import React, { useState, useContext } from "react";
import db from "../firebase.js";
import firebase from "firebase";
import StateContext from "../StateContext";

export default function FollowingUser({ avatar, username, uid }) {
  const { mainUserId, followingUsers } = useContext(StateContext);
  const [follows, setFollows] = useState(followingUsers);

  const removeFollow = e => {
    const userElement = e.parentElement;
    const userToUnfollow = userElement.getAttribute("data-uid");

    db.collection("users")
      .doc(mainUserId)
      .update({
        following: firebase.firestore.FieldValue.arrayRemove(userToUnfollow)
      })
      .then(() => {
        const newFollows = follows.filter(follow => follow !== userToUnfollow);
        setFollows(newFollows);
      });
    userElement.remove();
  };

  return (
    <article data-uid={uid} className="following-user">
      <figure className="following-user__avatar">
        <img src={avatar} alt="Profile picture" />
      </figure>
      <h2 className="following-user__username">{username}</h2>
      <button>View messages</button>
      {uid == mainUserId ? "" : <button onClick={e => removeFollow(e.target)}>Unfollow</button>}
    </article>
  );
}
 

Компонент стены:

 import React, { useState, useEffect, useContext } from "react";
import db from "../firebase.js";
import Post from "./Post";
import StateContext from "../StateContext";

export default function Wall() {
  const { followingUsers } = useContext(StateContext);
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    console.log(followingUsers);
    const readyToRender = Object.values(followingUsers).length > 0;
    if (readyToRender) {
      db.collection("posts")
        .where("user_id", "in", followingUsers)
        .orderBy("timestamp", "desc")
        .get()
        .then(posts => setPosts(posts.docs.map(post => post.data())));
    }
  }, [followingUsers]);

  return (
    <section id="wall">
      <div className="window">
        <h1 className="window__title">Latest messages</h1>
        <div className="window__content">
          {posts.map((post, index) => (
            <Post avatar={post.user_avatar} username={post.username} uid={post.user_id} body={post.body} timestamp={post.timestamp.toDate().toDateString()} key={index} />
          ))}
        </div>
      </div>
    </section>
  );
}
 

StateContext.js:

 import { createContext } from "react";

const StateContext = createContext();

export default StateContext;

 

Комментарии:

1. Как вы реализовали свой StateContext?

2. @ImranRafiqRather Я добавил это в конце основного сообщения.

3. Хм. Да, я проверил.. Позвольте мне еще раз проверить код 🙂 Я надеюсь, что все данные поступают из firestore правильно…

4. Да, это так. Сейчас я вижу, что setFollows в FollowingUser по какой-то причине фактически не обновляет состояние.

5. Я создал аналогичную демо-версию 🙂 Просто дай мне минутку, чтобы посмотреть, что происходит, приятель 🙂

Ответ №1:

Основная проблема заключается в настройке state переменных в Main.js файле (эти данные фактически должны быть частью состояния Context для обработки глобально).

Приведенный ниже код не обновит наше состояние глобально.

    const globalValues = {
     mainUserId: mainUser,
     followingUsers: follows
   };
 

Мы должны записать состояние таким образом, чтобы оно изменялось на уровне глобального контекста.
Итак, в пределах вашего Main.js установленного состояния, как показано ниже:

   const [globalValues, setGlobalValues] = useState({
    mainUserId: "uid_MainUser",
    followingUsers: []
  });
 

Также добавьте все свои event handlers Context Level in Main.js только для того, чтобы избежать prop-drilling и улучшить работу.

CODESAND BOX DEMO: https://codesandbox.io/s/context-api-and-rendereing-issue-uducc

Демонстрация фрагмента кода:

 import React, { useEffect, useState } from "react";
import FollowingSidebar from "./FollowingSidebar";

import StateContext from "./StateContext";
const url = "https://jsonplaceholder.typicode.com/users";

function App() {
  const [globalValues, setGlobalValues] = useState({
    mainUserId: "uid_MainUser",
    followingUsers: []
  });

  const getUsers = async (url) => {
    const response = await fetch(url);
    const data = await response.json();

    setGlobalValues({
      ...globalValues,
      followingUsers: data
    });
  };

  // Acts similar to componentDidMount now :) Called only initially
  useEffect(() => {
    getUsers();
  }, []);

  const handleClick = (id) => {
    console.log(id);
    const updatedFollowingUsers = globalValues.followingUsers.filter(
      (user) => user.id !== id
    );
    setGlobalValues({
      ...globalValues,
      followingUsers: updatedFollowingUsers
    });
  };

  return (
    <StateContext.Provider value={{ globalValues, handleClick }}>
      <FollowingSidebar />
    </StateContext.Provider>
  );
}

export default App;