reactjs React惯用的受控输入(useCallback、props和scope)

k4emjkb1  于 2023-01-08  发布在  React
关注(0)|答案(1)|浏览(117)

当我发现我的输入在每次按键时都失去了焦点时,我正在构建一个很好的旧的读取-获取-建议查找栏。
我了解到,因为我的输入组件是在包含它的头组件内部定义的,所以对状态变量的更改会触发父组件的重新呈现,而父组件又会重新定义输入组件,从而导致这种行为。使用useCallback可以避免这种情况。
现在,状态值仍然是一个空字符串,即使调用了它的回调(正如我看到的console.log的击键)
通过将state和state setter作为props传递给input组件可以修复这个问题。**但是我不太明白为什么。**我猜测包含在useCallback中的state和setter与后续调用产生的状态和setter "断开"。
我很高兴能读到一个解释来澄清这一点。为什么它以一种方式工作而不是另一种方式?当使用usecallback时,封闭作用域是如何测量的?
这是密码。

export const Header = () => {

    const [theQuery, setTheQuery] = useState("");
    const [results, setResults] = useState<ResultType>();

    // Query

    const runQuery = async () => {
        const r = await fetch(`/something`);
        if (r.ok) {
            setResults(await r.json());
        }
    }

    const debouncedQuery = debounce(runQuery, 500);

    useEffect(() => {
        if (theQuery.length > 3) {
            debouncedQuery()
        }
    }, [theQuery]);


    const SearchResults = ({ results }: { results: ResultType }) => (
        <div id="results">{results.map(r => (
            <>
                <h4><a href={`/linkto/${r.id}`}>{r.title}</a></h4>
                {r.matches.map(text => (
                    <p>{text}</p>
                ))}
            </>
        ))}</div>
    )

    // HERE
    // Why does this work when state and setter go as 
    // props (commented out) but not when they're in scope?

    const Lookup = useCallback((/* {theQuery, setTheQuery} : any */) => {

        return (
            <div id='lookup_area'>

                <input id="theQuery" value={theQuery}
                    placeholder={'Search...'}
                    onChange={(e) => {
                        setTheQuery(e.target.value);
                        console.log(theQuery);
                    }}
                    type="text" />
            </div>
        )
    }, [])

    return (
        <>
            <header className={`${results ? 'has_results' : ''}`}>

                <Lookup /* theQuery={theQuery} setTheQuery={setTheQuery} */ />

            </header>

            {results && <SearchResults results={results} />}
        </>
    )
}
7z5jn7bk

7z5jn7bk1#

在另一个组件的render函数中包含本质上是组件的定义通常不是一个好主意,因为您已经遇到了所有的挑战,但是我会回到这个问题并回答您最初的问题。
执行以下操作时:

const Lookup = useCallback((/* {theQuery, setTheQuery} : any */) => {

        return (
            <div id='lookup_area'>

                <input id="theQuery" value={theQuery}
                    placeholder={'Search...'}
                    onChange={(e) => {
                        setTheQuery(e.target.value);
                        console.log(theQuery);
                    }}
                    type="text" />
            </div>
        )
    }, [])

基本上就是说,当Header组件挂载时,在Lookup中存储一个返回JSX的函数,并在组件 * mount * 时将其放入缓存中,在后续呈现时从不刷新。react将从初始呈现中提取定义,而不是重新定义函数--这称为记忆。这意味着Lookup在所有渲染之间将是参考稳定的。
deps数组[]定义了它只在挂载上,deps数组是一个列表,当在渲染之间更改时,将触发回调重新定义,同时将从当前呈现中拉入其所包含的组件的新范围。由于您没有列出deps数组内父作用域的回调函数内使用的所有内容,你实际上是在解决一个bug,如果你使用正确的提升规则,这个bug会被标记出来,这是因为如果内部使用的东西,比如theQuerysetTheQuery发生了变化,那么Lookup回调将不会刷新/重新定义--它将使用挂载时存储在本地缓存中的值,而本地缓存又会引用这些值的旧副本。
也就是说,既然您这样做了,Lookup就保持稳定,不会被刷新。正如您所说的,如果它刷新了,您将看到它被重新挂载,并看到它的隐式DOM状态在react中,组件定义需要是引用稳定的。您的useCallback基本上是组件定义,但在另一个组件呈现中,因此,它留给你来处理棘手的记忆业务,以"防止"它被重新定义的每一个渲染。我会回来,你如何正确地解决这个问题很快。
当添加theQuery={theQuery} setTheQuery={setTheQuery}时,您通过将此数据从父作用域传递到回调函数来解决实际的根本问题,这样它就不需要使用从初始呈现时就已存在的陈旧数据。
但是您所做的实际上是在另一个组件中编写一个组件,这使得封装更少,并产生了您所看到的问题。您只需要将Lookup定义为它自己的组件。还有SearchResults

const Lookup = ({theQuery, setTheQuery}: any)) => {
        return (
            <div id='lookup_area'>

                <input id="theQuery" value={theQuery}
                    placeholder={'Search...'}
                    onChange={(e) => {
                        setTheQuery(e.target.value);
                        console.log(theQuery);
                    }}
                    type="text" />
            </div>
        )
}

const SearchResults = ({ results }: { results: ResultType }) => (
    <div id="results">{results.map(r => (
        <>
            <h4><a href={`/linkto/${r.id}`}>{r.title}</a></h4>
            {r.matches.map(text => (
                <p>{text}</p>
            ))}
        </>
    ))}</div>
 )

export const Header = () => {

    const [theQuery, setTheQuery] = useState("");
    const [results, setResults] = useState<ResultType>();

    // Query

    const runQuery = async () => {
        const r = await fetch(`/something`);
        if (r.ok) {
            setResults(await r.json());
        }
    }

    const debouncedQuery = debounce(runQuery, 500);

    useEffect(() => {
        if (theQuery.length > 3) {
            debouncedQuery()
        }
    }, [theQuery]);

    return (
        <>
            <header className={`${results ? 'has_results' : ''}`}>

                <Lookup theQuery={theQuery} setTheQuery={setTheQuery} />

            </header>

            {results && <SearchResults results={results} />}
        </>
    )
}

由于组件是在render之外定义的,因此它们只定义了一次,如果不通过props传递给Header组件,它们就不能直接访问Header组件的作用域。这是一个正确封装和参数化的组件,消除了陷入作用域和记忆混乱的机会。
将组件封装在组件中只是为了访问作用域是不可取的。这是错误的思维方式。您应该尽可能合理地分解组件。您总是可以在一个文件中包含多个组件。在React中,组件不仅是重用的单元,而且是封装逻辑的主要方式。它应该在这种情况下使用。

相关问题