简体   繁体   English

React - 如何使用另一个组件重新渲染一个组件?

[英]React - How to re-render a component using another component?

I have a NavBar component that has a list of dynamically generated links (these links are generated after querying my backend for some categories ).我有一个NavBar组件,其中包含动态生成的链接列表(这些链接是在查询我的后端的某些categories后生成的)。 These links are stored inside a child component of the NavBar, called DrawerMenu .这些链接存储在 NavBar 的子组件中,称为DrawerMenu

The NavBar is a child of the main App.js component. NavBar是主App.js组件的子组件。

In my Category component, I have a "delete" function that deletes a category.在我的Category组件中,我有一个删除类别的“删除”function。 Once I delete a category I want to remove the link to it in the NavBar .一旦我删除了一个类别,我想在NavBar中删除指向它的链接。 How would I go about doing this?我将如何 go 这样做?

For further context, my components are given below:对于进一步的上下文,我的组件如下所示:

DrawerMenu component DrawerMenu 组件

class DrawerMenu extends Component {
  state = {
    menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
  }

  getData = (query) => {
    // Query backend for category data and set it to this.state.menuItems
  }

  componentDidMount() {
    this.getData(menuItemsQuery)
  }

  render() {
    const { classes, handleDrawerClose, open } = this.props
    const { menuItems } = this.state

    const drawer = (classes, handleDrawerClose) => (
      <div>
          ...

          {
            menuItems.map((menuItem, index) => (
              <Link color="inherit" key={index} to={menuItem.link} className={classes.drawerLink} component={RouterLink}>
                <ListItem button className={classes.drawerListItem} onClick={handleDrawerClose}>
                  <ListItemText primary={menuItem.name} />
                </ListItem>
              </Link>
            ))
          }
          
          ...
      </div>
    )
    
    ...

    return (
      <div>
        <Drawer
          variant="temporary"
          anchor='left'
          open={open}
          onClose={handleDrawerClose}
          classes={{
            paper: `${open ? classes.drawerOpen : null} ${!open ? classes.drawerClose : null}`,
          }}
          ModalProps={{
            keepMounted: true, // Better open performance on mobile.
          }}
        >
          {drawer(classes, handleDrawerClose)}
        </Drawer>
      </div>
    )
  }
}

NavBar component导航栏组件

function PrimarySearchAppBar(props) {
    return (
        <div className={classes.grow}>
            
            ...
            
            <DrawerMenu
                classes={classes}
                handleDrawerClose={handleDrawerClose}
                open={open}
            />
            
            ...
        </div>
    )
}

Category component类别组件

class Category extends Component {
    ...
    
    deleteCategory = async () => {
        // Code to request backend to delete category
        this.props.history.push(`/`)
    }
    
    ...
}

There are two common ways of doing this: You can either use a state management tool, like Redux or pass your state down the component tree as props.有两种常见的方法:您可以使用 state 管理工具,如Redux或将您的 state 作为道具传递到组件树中。

Redux is often used when several components depend on the same state or when the component that depends on a state is several layers deep, so it would get cumbersome to pass it down as props. Redux 常用于多个组件依赖于同一个 state 或依赖于 state 的组件向下传递好几层时,传递它会很麻烦。

I'll assume your component tree is not very large, so I will create a simple example passing props down the tree.我假设您的组件树不是很大,所以我将创建一个简单的示例,将道具向下传递。

class DrawerMenu extends Component {
  // We're gonna manage the state here, so the deletion
  // will actually be handled by this component
  state = {
    menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
  }

  handleDelete = (id) => {
    let updatedMenuItem = [...this.state.menuItems]; //Create a copy
    updatedMenuItem = updatedMenuItem(item => item.id !== id) // Remove the 
deleted item
    this.setState({
       menuItems: updatedMenuItem
    })    
   
  }
  ...

   // Then wherever you render the category component
   <Category handleDelete = {handleDelete}/> //Pass a reference to the delete method

}


Category Component类别组件

 class Category extends Component {
    ...
    
    deleteCategory = async () => {
        // Code to request backend to delete category
        this.props.handleDelete(categoryId) //Pass the id of the category
        this.props.history.push(`/`)
    }
    
    ...
}

I would suggest reading about state management, it is a core concept in React and you will use it everywhere.我建议阅读有关 state 管理的内容,它是 React 中的核心概念,您将在任何地方使用它。 Redux and Context API for example.例如 Redux 和上下文 API。

Not sure why Dennis Vash deleted their answer, they are correct, but perhaps not descriptive enough in the solution.不知道为什么 Dennis Vash 删除了他们的答案,他们是正确的,但在解决方案中可能没有足够的描述性。

The way you delete the category is not to call the backend itself from inside the category component, because then the navbar doesn't know that you made a call, but to call a callback that is in an ancestor shared by both the category component and the navbar to delete a category, and then rerequest the categories list from the server.删除类别的方法不是从类别组件内部调用后端本身,因为导航栏不知道您进行了调用,而是调用类别组件和共享的祖先中的回调导航栏删除一个类别,然后从服务器重新请求类别列表。 In the example below, this ancestor that is shared is MyCategoriesProvider在下面的示例中,这个共享的祖先是MyCategoriesProvider

Because the category component is likely to be in a much different place (or multiple places) in the tree than the NavBar, it's best to use context.因为类别组件可能在树中的位置(或多个位置)与 NavBar 大不相同,所以最好使用上下文。

Honestly, this is a great place for redux, but I'm not going to push redux on you and instead will just demo a Context solution.老实说,这是 redux 的好地方,但我不会向你推销 redux,而只是演示一个 Context 解决方案。

// We're going to create a context that will manage your categories
// The only job of this context is to hold the current categories, 
// and supply the updating functions. For brevity, I'll just give 
// it a handleDelete function.
// Ideally, you'd also store the status of the request in this context
// as well so you could show loaders in the app, etc

import { createContext } from 'react';

// export this, we'll be using it later
export const CategoriesContext = createContext();

// export this, we'll render it high up in the app
// it will only accept children
export const MyCategoriesProvider = ({children}) => {

   // here we can add a status flag in case we wanted to show a spinner
   // somewhere down in your app
   const [isRequestingCategories,setIsRequestingCategories] = useState(false);

   // this is your list of categories that you got from the server
   // we'll start with an empty array
   const [categories,setCategories] = useState([]);

   const fetch = async () => {
      setIsRequestingCategories(true);
      setCategories(await apiCallToFetchCategories());
      setIsRequestingCategories(false);
   }

   const handleDelete = async category => {
       await apiCallToDeleteCategory(category);
       // we deleted a category, so we should re-request the list from the server
       fetch();
   }

   useEffect(() => {
      // when this component mounts, fetch the categories immediately
      fetch();

      // feel free to ignore any warnings if you're using a linter about rules of hooks here - this is 100% a "componentDidMount" hook and doesn't have any dependencies
   },[]);

   return <CategoriesContext.Provider value={{categories,isRequestingCategories,handleDelete}}>{children}</CategoriesContext.Provider>

}

// And you use it like this:

const App = () => {
  return (
    <MyCategoriesProvider>
      <SomeOtherComponent>
      <SomeOtherComponent> <- let's say your PrimarySearchBar is in here somewhere
      <SomeOtherComponent>
    </MyCategoriesProvider>
  )

}

// in PrimarySearchBar you'd do this:

function PrimarySearchBar(props) => {
   const {categories} = useContext(CategoriesContext); // you exported this above, remember?
   
   // pass it as a prop to navbar, you could easily put the useContext hook inside of any component
   return <NavBar categories={categories}/>

}


// in your category component you could do this:

class Category extends Component {
     render() {
        // Don't forget, categoriesContext is the thing you exported way up at the top
        <CategoriesContext.Consumer>
           {({handleDelete}) => {
                return <button onClick={() => handleDelete(this.props.category)}>
           }}
        </CategoriesContext.Consumer>
     }
}

EDIT:编辑:

I see you're mixing class and functional components, which is fine.我看到您正在混合 class 和功能组件,这很好。 You should check out this article on how to use the context api in either of them - in functional components you typically use a useContext hook, while in class components you'll use a consumer.您应该查看这篇文章,了解如何在其中任何一个中使用上下文 api - 在功能组件中您通常使用useContext挂钩,而在 class 组件中您将使用消费者。

I would just refresh the list of categories that come from the server, after the delete request is done.在删除请求完成后,我会刷新来自服务器的类别列表。

I'd do it as follows:我会这样做:

  • I would make the drawer component not so smart, making it receive the list of menuItems.我会让抽屉组件不那么聪明,让它接收菜单项列表。
<DrawerMenu
    classes={classes}
    handleDrawerClose={handleDrawerClose}
    open={open}
    items={/* ... */}
/>

This is an important step, because now, to refresh the list of items rendered, you just pass another list.这是一个重要的步骤,因为现在,要刷新呈现的项目列表,您只需传递另一个列表。 The server-side logic remains disconnected from this component in this way.以这种方式,服务器端逻辑与该组件保持断开连接。

  • I'm not sure where you render the Category components, but supposing it is rendered outside the PrimarySearchAppBar it seems that this menuItems might need to be passed to the components from an upper level.我不确定您在哪里呈现Category组件,但假设它呈现在PrimarySearchAppBar之外,似乎这个 menuItems 可能需要从上层传递给组件。 I see 2 solutions:我看到了 2 个解决方案:

    1. I'd do the request for the menuItems from the same place where I do the request for the categories:我会从我请求类别的同一个地方请求 menuItems:
const App = props => {
    const [categories, setCategories] = React.useState([])
    const [menuItems, setMenuItems] = React.useState([])

    const fetchCategories = useCallback(()=> {
        yourApi.getCategories().then(categories => setCategories(categories))
    })

    const fetchMenuItems = useCallback(() => {
        yourApi.getMenuItems().then(menuItems => setMenuItems(menuItems))
    })

    useEffect(() => {
        fetchCategories()
    }, [])

    useEffect(() => {
       fetchMenuItems()
    }, [categories])

    const handleDeleteCategory = useCallback(idToDelete => {
        yourApi.deleteCategory(idToDelete).then(fetchCategories)
    })

    return (
        <div>
             <PrimarySearchAppBar menuItems={menuItems}/>
             <Categories categories={categories} onDeleteClick={handleDeleteCategory} />
        </div>
    )

}
  1. you can do the same thing but do it with a provider and using the content API if you do not want to have all the logic here.您可以做同样的事情,但如果您不想在这里拥有所有逻辑,请使用提供程序并使用内容 API 来做。 It is good to have smart/fetches/server-side logic in a top level component and then pass down props to dumb components.最好在顶层组件中有智能/获取/服务器端逻辑,然后将 props 传递给哑组件。

PS. PS。 There is also a nice hook to make fetches easier: https://github.com/doasync/use-promise还有一个很好的钩子可以让获取更容易: https://github.com/doasync/use-promise

I currently use a custom version of a usePromise hook I found because I added some interesting features.我目前使用我发现的 usePromise 挂钩的自定义版本,因为我添加了一些有趣的功能。 I can share it if you want but I don't want to add noise to the answer.如果您愿意,我可以分享它,但我不想在答案中添加噪音。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM