【问题标题】:how to get data from api only when users stop typing and/or when users click enter key仅当用户停止输入和/或用户单击输入键时如何从 api 获取数据
【发布时间】:2021-01-18 23:48:32
【问题描述】:

下面是我的 2 个 js 文件。

我将一个方法传递给子 SearchBox,从其中我从搜索文本字段中检索值并在父级 CourseList.js 中调用该方法。当我取回父级内部的值时,我将该值传递给 fetch api 调用作为从 api 检索数据的查询参数。

我只想在用户停止输入 2 秒和/或当用户单击回车键时从 api 获取数据。现在它只是在输入更改时从 api 获取数据,所以我不想要多次调用 api。

这是我的代码,

课程列表.js

import React, { useState, useEffect, useContext } from "react";
import { Link } from "react-router-dom";

import {
  List,
  ListItem,
  ListItemAvatar,
  ListItemText,
  Typography,
  Box,
  Divider,
  Grid,
  Button,
  TablePagination,
} from "@material-ui/core";

import { makeStyles } from "@material-ui/core/styles";

import LanguageDropdown from "./LanguageDropdown";
import RatingDropdown from "./RatingDropdown";
import CategoryDropdown from "./CategoryDropdown";
import LevelDropdown from "./LevelDropdown";
import StreamDropdown from "./StreamDropdown";

import SearchBox from "./SearchBox";
import CourseSort from "./CourseSort";

import Pagination from "@material-ui/lab/Pagination";

import settings from "../../../../settings";

const getTruncated = (items, value) => {
  // console.log("coming from getTruncated func ", items);

  let cut = items.indexOf(" ", value);
  if (cut == -1) {
    return items;
  }
  return items.substring(0, cut);
};


export default function CourseList(props) {
  const classes = useStyles();

  const [myCourses, setMyCourses] = useState([]);
  const [page, setPage] = useState(0);
  const [cate, setCate] = useState(true);
  const [rowsPerPage, setRowsPerPage] = useState(5);

  const [search, setSearch] = useState("");
  const [userLessons, setUserLessons] = useState([]);

  const handleChangePage = (event, newPage) => {
    setPage(newPage);
  };

  const handleChangeRowsPerPage = (event) => {
    setRowsPerPage(+event.target.value);
    setPage(0);
  };

  useEffect(() => {
    let mounted = true;

    if (window.location.pathname !== "/portal/ondemand") {
      setCate(false);
    }

    setMyCourses(props.userCourses);
    getInitSignon();

    return () => {
      mounted = false;
    };
  }, [search]);

  const getInitSignon = () => {
    const token = localStorage.getItem("cars-crm-token");

    fetch(settings.api().lessons + `?SearchText=${search}`, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    })
      .then((res) => {
        if (res.status === 200) {
          return res.json();
        } else {
          const error = new Error(res.error);
          throw error;
        }
      })
      .then((data) => {
        console.log("coming from onDemandcourse", data);
        setUserLessons(data);
      })
      .catch((err) => {
        console.error(err);
      });
  };

  const searchedItem = (value) => {
    setSearch(value)
  };


  return (
    <>
      <Grid container className={classes.searchsort}>
        <Grid item>
          <SearchBox
            className={classes.searchContainer}
            handleSearched={searchedItem}
          />
        </Grid>

      </Grid>

      <Grid className={classes.root}>
        <Grid item className={classes.filters}>
          <Grid direction="column">
            <LanguageDropdown courses={myCourses} />

            {cate && <CategoryDropdown />}

          </Grid>
        </Grid>

        <Grid item>
          <List className={classes.courses}>
            {userLessons
              .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
              .map((lesson) => (
                <>
                  <div key={lesson.lessonId}>
                    <ListItem
                      style={{
                        display: "flex",
                        alignItems: "flex-start",
                        justifyContent: "center",
                        backgroundColor: "#fff",
                        padding: "16px 15px",
                      }}
                    >
                      <ListItemAvatar
                        component={Link}
                        to={`/portal/ondemand/lessons/${lesson.lessonId}`}
                      >
                        <Link
                          to={`/portal/ondemand/lessons/${lesson.lessonId}`}
                        >
                          <img
                            src={lesson.lessonThumbUrl}
                            className={classes.img}
                            alt={lesson.lessonTitle}
                          />
                        </Link>
                      </ListItemAvatar>

                      <ListItemText
                        style={{ margin: "0px" }}
                        primary={
                          <React.Fragment>
                            <Typography
                              component="span"
                              variant="body2"
                              className={classes.inline}
                              color="textPrimary"
                            >
                              <Box fontSize={20}>
                                <Typography
                                  className={classes.courseTitle}
                                  variant="h6"
                                >
                                  {lesson.lessonTitle}
                                </Typography>
                              </Box>
                              <Grid
                                container
                                direction="row"
                                justify="space-between"
                                style={{ margin: "12px 0", maxWidth: "50rem" }}
                              >
                                <Grid item>
                                  <Box fontStyle="italic">
                                    <Typography variant="h6">
                                      Code: {lesson.courseCode} &nbsp; | &nbsp;
                                      {lesson.lessonCreated
                                        ? lesson.lessonCreated.split("T")[0]
                                        : undefined}{" "}
                                      &nbsp; | &nbsp;Course:{" "}
                                      {lesson.courseTitle}
                                    </Typography>
                                  </Box>
                                </Grid>
                              </Grid>
                            </Typography>
                          </React.Fragment>
                        }
                        secondary={
                          <Box>
                            <Typography
                              variant="body1"
                              style={{ marginBottom: "12px" }}
                            >
                              {getTruncated(lesson.lessonDescription, 150) +
                                "..."}
                            </Typography>
                            <Button
                              className={classes.startLesson}
                              edge="start"
                              variant="contained"
                              href={`/portal/ondemand/lessons/${lesson.lessonId}`}
                            >
                              Start Lesson
                            </Button>
                          </Box>
                        }
                      />
                    </ListItem>

                    <Divider className={classes.divider} />
                  </div>
                </>
              ))}
          </List>

          <TablePagination
            rowsPerPageOptions={[10, 50, 100]}
            component="div"
            count={userLessons.length}
            rowsPerPage={rowsPerPage}
            page={page}
            onChangePage={handleChangePage}
            onChangeRowsPerPage={handleChangeRowsPerPage}
          />
        </Grid>
      </Grid>
    </>
  );
}

搜索框.js

import React from "react";
import { TextField } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";

const useStyles = makeStyles((theme) => ({
  TextField: {
    width: "100%",
  },
  menu: {
    width: 200,
  },
}));


export default function SearchBox(props) {

  const handlechangeSearch = (event) => {
    props.handleSearched(event.target.value);
  };

  const classes = useStyles();

  return (
    <React.Fragment>
      <TextField
        id="outlined-search"
        label="Search"
        className={classes.TextField}
        onChange={handlechangeSearch}
        margin="dense"
        variant="outlined"
      />
    </React.Fragment>
  );
}

需要问题的解决方案,我将不胜感激。提前谢谢!

【问题讨论】:

  • 我在父组件中添加了另外 2 个函数,1 个用于 keydown,1 个用于使用 settimeout 进行更改。现在我的 api 调用正在等待 2 秒。但是,按回车键有效,但 2 秒后再次调用 api,并使用默认结果重新渲染它并返回输入的结果。 ```
  • 您需要对 api 调用进行去抖动处理。 Loadash 库具有您可以使用的去抖动实用程序,或者您可以自己编写。你甚至可以编写自己的 useDebounce 钩子。该视频展示了如何制作 useDebounce 钩子,它非常有见地,因为它向您展示了在对异步 api 调用进行去抖动时可能遇到的一些问题:youtube.com/watch?v=EXpLFRGM8kg

标签: reactjs api


【解决方案1】:

我在父组件中添加了另外 2 个功能 1 用于 keydown 和 1 调整 searchedItem 并在父组件中设置超时。现在我的 api 调用正在等待 2 秒。

但是,按 Enter 键可以正常工作,但 2 秒后 API 会再次被调用,它会使用默认值重新渲染组件,然后像一闪而过一样返回输入的结果。

下面是函数,

课程列表.js

import React, { useState, useEffect, useContext } from "react";

import { Link } from "react-router-dom";

import {
  List,
  ListItem,
  ListItemAvatar,
  ListItemText,
  Typography,
  Box,
  Divider,
  Grid,
  Button,
  TablePagination,
} from "@material-ui/core";

import { makeStyles } from "@material-ui/core/styles";

import LanguageDropdown from "./LanguageDropdown";
import RatingDropdown from "./RatingDropdown";
import CategoryDropdown from "./CategoryDropdown";
import LevelDropdown from "./LevelDropdown";
import StreamDropdown from "./StreamDropdown";

import SearchBox from "./SearchBox";
import CourseSort from "./CourseSort";

import Pagination from "@material-ui/lab/Pagination";


import settings from "../../../../settings";


const getTruncated = (items, value) => {
  // console.log("coming from getTruncated func ", items);

  let cut = items.indexOf(" ", value);
  if (cut == -1) {
    return items;
  }
  return items.substring(0, cut);
};


export default function CourseList(props) {
  const classes = useStyles();

  const [myCourses, setMyCourses] = useState([]);
  const [page, setPage] = useState(0);
  const [cate, setCate] = useState(true);
  const [rowsPerPage, setRowsPerPage] = useState(5);

  const [search, setSearch] = useState("");
  const [userLessons, setUserLessons] = useState([]);
  const [delayDebounceFn, setDelayDebounceFn] = useState(0);

  const handleChangePage = (event, newPage) => {
    setPage(newPage);
  };

  const handleChangeRowsPerPage = (event) => {
    setRowsPerPage(+event.target.value);
    setPage(0);
  };

  useEffect(() => {
    let mounted = true;

    console.log("coming from useffect course list", window.location.pathname);

    if (window.location.pathname !== "/portal/ondemand") {
      setCate(false);
    }

    setMyCourses(props.userCourses);

    getInitSignon();

    return () => {
      mounted = false;
    };
  }, [search]);

  const getInitSignon = () => {
    // initSignon
    const token = localStorage.getItem("cars-crm-token");

    console.log("coming after being search inside init", search);
    fetch(settings.api().lessons + `?SearchText=${search}`, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    })
      .then((res) => {
        if (res.status === 200) {
          console.log("coming from course category", res);
          return res.json();
        } else {
          const error = new Error(res.error);
          throw error;
        }
      })
      .then((data) => {
        console.log("coming from onDemandcourse", data);
        setUserLessons(data);
      })
      .catch((err) => {
        console.error(err);
      });
  };

  const searchedItem = (value) => {
    setTimeout(() => setSearch(value), 2000);
  };

  const keyDownSearched = (value) => {
    setSearch(value);
  };

  return (
    <>
      <Grid container className={classes.searchsort}>
        <Grid item>
          <SearchBox
            className={classes.searchContainer}
            handleSearched={searchedItem}
            keydownSearched={keyDownSearched}
          />
        </Grid>

        <Grid item>
          <CourseSort className={classes.sortContainer} />
        </Grid>
      </Grid>

      <Grid className={classes.root}>
        <Grid item className={classes.filters}>
          <Grid direction="column">
            <LanguageDropdown courses={myCourses} />

            {cate && <CategoryDropdown />}

          </Grid>
        </Grid>

        <Grid item>
          <List className={classes.courses}>
            {userLessons
              .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
              .map((lesson) => (
                <>
                  <div key={lesson.lessonId}>
                    <ListItem
                      style={{
                        display: "flex",
                        alignItems: "flex-start",
                        justifyContent: "center",
                        backgroundColor: "#fff",
                        padding: "16px 15px",
                      }}
                    >
                      <ListItemAvatar
                        component={Link}
                        to={`/portal/ondemand/lessons/${lesson.lessonId}`}
                      >
                        <Link
                          to={`/portal/ondemand/lessons/${lesson.lessonId}`}
                        >
                          <img
                            src={lesson.lessonThumbUrl}
                            className={classes.img}
                            alt={lesson.lessonTitle}
                          />
                        </Link>
                      </ListItemAvatar>

                      <ListItemText
                        style={{ margin: "0px" }}
                        primary={
                          <React.Fragment>
                            <Typography
                              component="span"
                              variant="body2"
                              className={classes.inline}
                              color="textPrimary"
                            >
                              <Box fontSize={20}>
                                <Typography
                                  className={classes.courseTitle}
                                  variant="h6"
                                >
                                  {lesson.lessonTitle}
                                </Typography>
                              </Box>
                              <Grid
                                container
                                direction="row"
                                justify="space-between"
                                style={{ margin: "12px 0", maxWidth: "50rem" }}
                              >
                                <Grid item>
                                  <Box fontStyle="italic">
                                    <Typography variant="h6">
                                      Code: {lesson.courseCode} &nbsp; | &nbsp;
                                      {lesson.lessonCreated
                                        ? lesson.lessonCreated.split("T")[0]
                                        : undefined}{" "}
                                      &nbsp; | &nbsp;Course:{" "}
                                      {lesson.courseTitle}
                                    </Typography>
                                  </Box>
                                </Grid>
                              </Grid>
                            </Typography>
                          </React.Fragment>
                        }
                        secondary={
                          <Box>
                            <Typography
                              variant="body1"
                              style={{ marginBottom: "12px" }}
                            >
                              {getTruncated(lesson.lessonDescription, 150) +
                                "..."}
                            </Typography>
                            <Button
                              className={classes.startLesson}
                              edge="start"
                              variant="contained"
                              href={`/portal/ondemand/lessons/${lesson.lessonId}`}
                            >
                              Start Lesson
                            </Button>
                          </Box>
                        }
                      />
                    </ListItem>

                    <Divider className={classes.divider} />
                  </div>
                </>
              ))}
          </List>

          <TablePagination
            rowsPerPageOptions={[10, 50, 100]}
            component="div"
            count={userLessons.length}
            rowsPerPage={rowsPerPage}
            page={page}
            onChangePage={handleChangePage}
            onChangeRowsPerPage={handleChangeRowsPerPage}
          />
        </Grid>
      </Grid>
    </>
  );
}

SearhcBox.js

import React, { useState, useEffect } from "react";
import { TextField } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";

const useStyles = makeStyles((theme) => ({
  // container: {
  //     display: 'flex',
  //     flexWrap: 'wrap',
  //   },
  TextField: {
    width: "100%",
  },
  menu: {
    width: 200,
  },
}));

// go into CourseFilterBar
export default function SearchBox(props) {
  const keyDownSearched = (event) => {
    if (event.key === "Enter") {
      props.keydownSearched(event.target.value);
    }

    console.log("coming from search box", event.target.value);
  };

  const handlechangeSearch = (event) => {
    props.handleSearched(event.target.value);
    console.log("coming from search box", event.target.value);
  };

  const classes = useStyles();

  return (
    <React.Fragment>
      <TextField
        id="outlined-search"
        label="Search"
        className={classes.TextField}
        onChange={handlechangeSearch}
        onKeyDown={keyDownSearched}
        margin="dense"
        // fullWidth={true}
        variant="outlined"
      />
    </React.Fragment>
  );
}

【讨论】:

    【解决方案2】:

    下面的代码在用户停止输入或用户按下回车键时进行 API 调用,如果用户按下回车或停止输入,则不重复 API 调用,也许有帮助

    import React,{useState,useEffect} from "react";
    
    export default function App() {
      const [searchText, setSearchText] = useState('')
      let delay;
    
    
    const callApi = ()=> {
      console.log('call Api',searchText)
    
    }
    
    
      useEffect(() => {
         delay = setTimeout(() => {
          if(searchText)callApi()
        }, 1000)
    
        return () => clearTimeout(delay)
      }, [searchText])
    
      const keyDown = (e)=>{
        if (e.key === "Enter") {
          clearTimeout(delay)
          callApi()
          console.log('keyDown press and ready for api call')
        }
      }
      return (
        <div>
          <input
          value={searchText}
          type='text'
          placeholder='Search here...'
          onChange={(e) => setSearchText(e.target.value)}
          onKeyDown={keyDown}
        />
        </div>
      );
    }
    

    【讨论】:

    • 非常感谢先生的帮助,它有效!感谢您的帮助和努力,我希望我能给您一杯咖啡以表示感谢!
    • Nabil 谢谢,很高兴知道它有帮助
    【解决方案3】:

    您需要使用debouncing 来实现这一点。请参阅here 示例,了解如何实现它。它在 React Native 中,但在 React JS 中是一样的,只是你使用 TextField 而不是 TextInput 并且事件的 props 有其他名称。

    import React, { useCallback, useState } from 'react';
    import { Text, TextInput, View } from 'react-native';
    import { debounce } from 'debounce';
    
    const makeRequest = term =>
      fetch(`https://postman-echo.com/get?term=${term}`).then(response =>
        response.json()
      );
    
    export default function App() {
      // State for keeping the last response from API
      const [response, setResponse] = useState('');
    
      // This makes the request immediately when called
      const getResponse = useCallback(term => {
        console.log('Make request with term: ', term);
        makeRequest(term).then(resp => setResponse(JSON.stringify(resp?.args)));
      }, []);
    
      // This schedules the getResponse function to be called after 2 seconds.
      // If in these 2 seconds debouncedGetResponse is called again, then it will
      // cancel the pending call and will schedule the new call to be run after
      // another 2 seconds.
      // We'll use this for onChangeText handler.
      const debouncedGetResponse = useCallback(debounce(getResponse, 2000), [
        getResponse,
      ]);
    
      // This will check for pending requests scheduled with debouncedGetResponse
      // and if there is a pending call it will be called immediately instead of
      // waiting those 2 seconds to finish.
      // We'll use this for onBlur handler.
      const debouncedGetResponseFlush = useCallback(
        () => debouncedGetResponse.flush(),
        [debouncedGetResponse]
      );
    
      return (
        <View style={styles.container}>
          <TextInput
            style={styles.textInput}
            onChangeText={debouncedGetResponse}
            onBlur={debouncedGetResponseFlush}
          />
          <Text>Response: {response}</Text>
        </View>
      );
    }
    
    const styles = {
      container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'stretch',
        backgroundColor: '#ecf0f1',
        padding: 8,
      },
      textInput: {
        height: 50,
        backgroundColor: '#ffffff',
        borderColor: '#cccccc',
        borderWidth: 1,
      },
    };
    

    【讨论】:

    • 感谢先生您对 debounced 的建议。我没有尝试这个 tbh,因为我已经使用 settimeout 进行了更改,这是由像你这样的另一个天才给出的。无论如何,我感谢您的帮助和努力。我真的很感激。
    猜你喜欢
    • 1970-01-01
    • 2020-06-05
    • 2017-07-02
    • 2011-03-07
    • 1970-01-01
    • 2021-07-14
    • 2022-12-09
    • 1970-01-01
    • 2022-07-21
    相关资源
    最近更新 更多