McD*_*erp 0 javascript reactjs react-hooks use-state use-ref
tl;dr - 如何通过跟踪强制重新渲染仅一个特定的子组件ref?
我有一个行表。我希望能够将鼠标悬停在行上并显示/隐藏行中的单元格,但只能在一段时间之后。
您只能在将鼠标悬停在整个表格上一段时间后(由onMouseEnter和触发)才能显示隐藏的悬停内容onMouseLeave。
一旦将鼠标悬停在特定的 上<Row>,如果父级允许,它应该显示额外的内容。
鼠标悬停在表格上的顺序:
isHovered现在是trueallowHover更改为trueallowHover和isHovered都是true,显示额外的行内容鼠标移出表格的顺序:
isHovered设置为falseallowHover更改为false此时,如果重新进入表,我们必须再次等待1秒才为allowHover真。一旦 和 都isHovered为allowHover真,则显示隐藏内容。一旦允许悬停,就不会有任何延迟:悬停在其上的行应立即显示隐藏的内容。
我试图useRef避免改变行父行的状态并导致所有子行的重新渲染
在行级别,悬停时,行应该能够检查是否允许悬停,而无需使用 props 重新渲染整个列表。我假设useEffect可以设置为跟踪该值,但它似乎不会在单个组件级别触发重新渲染。
换句话说,预期的行为是当前悬停在行上的行检测父级中的更改,并且仅重新渲染自身以显示内容。然后,一旦允许悬停,行为就很简单。将鼠标悬停在行上?揭示其内容。
以下是涉及的代码片段:
function Table() {
const allowHover = useRef(false);
const onMouseEnter = (e) => {
setTimeout(() => {
allowHover.current = true; // allow hovering
}, 1000);
};
const onMouseLeave = (e) => {
setTimeout(() => {
allowHover.current = false; // dont allow hovering
}, 1000);
};
return (
<div className="App" style={{ border: '3px solid blue' }}>
<h1>table</h1>
{/* allow/disallow hovering when entering and exiting the table, with a delay */}
<table onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<tbody>
<AllRows allowHover={allowHover} />
</tbody>
</table>
</div>
);
}
function Rows(props) {
return [1, 2, 3].map((id) => (
<Row id={id} allowHover={props.allowHover} />
));
}
function Row(props) {
let [isHovered, setIsHovered] = useState(false);
useEffect(() => {
// Why isn't this re-rendering this component?
}, [props.allowHover]);
const onMouseEnter = ({ target }) => {
setIsHovered(true);
};
const onMouseLeave = ({ target }) => {
setIsHovered(false);
};
console.log('RENDERING ROW');
return (
<tr key={props.id} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<td style={{ border: '1px solid red' }}>---------------- {props.id}</td>
<td style={{ border: '1px solid green' }}>
{props.allowHover.current && isHovered ? (
<button>ACTIONS</button>
) : null}
</td>
</tr>
);
}
Run Code Online (Sandbox Code Playgroud)
React最大的优点之一是,它允许我们管理具有状态的应用程序,而无需通过手动更新和事件侦听器与 DOM 交互。每当 React “重新渲染”时,它都会渲染一个虚拟 DOM,然后将其与实际 DOM 进行比较,在必要时进行替换。
useState:返回一个状态和一个setter,每当setter以新状态执行时,组件就会重新渲染。useRef:提供了在渲染之间保留对值或对象的引用的可能性。它就像一个变量的包装器,存储在引用的current属性中,并且只要您不更改它,它在渲染之间就引用相同。为 ref 设置新值不会导致组件重新渲染。您可以将实际的 DOM 节点附加到 refs,但您实际上可以将任何内容附加到 ref。useEffect:组件呈现后运行的代码。useEffect在 DOM 更新后执行。memo:增加了在父级重新渲染后子级重新渲染时手动控制的可能性。我假设这是某种玩具示例,并且您想了解 React 是如何工作的,如果不是,那么通过直接操作 DOM 节点来完成此操作并不是可行的方法。事实上,只有memo当无法以其他方式完成您想要的任务时,或者使用 React 的常规方式执行此操作无法产生可接受的性能时,您才应该使用性能改进和直接操作 DOM 节点。在你的情况下,性能就足够好了。
There are libraries that do most of the work outside of React. One of those is react-spring which animates DOM elements. Doing these animations in React would slow them down and make them lag, therefore react-spring uses refs and updates DOM nodes directly, setting different CSS properties right on the element.
useEffect in Row is not triggered whenever you change the content of the ref. Well, this is simply because useEffect runs after render, and there is no guarantee that the component will rerender just because you change the content of the allowHover ref. The ref passed to AllRows and Row is the same property all the time (only its current property changes), therefore they will never rerender due to props being changed. Since Row only rerenders, by itself, when isHovered is set, there is no guarantee that the useEffect will fire just because you change the content of allowHover ref. WHEN Row rerenders, the effect will run IF the value of allowHover.current is different from last time.memo will not help you here either since Table or AllRows don't rerender either. memo allows us to skip rerendering children when parents rerender, but here, the parents don't rerender, so memo will do nothing.All in all, neither useEffect or memo are some kind of magic functions that keep track of variables at all times and then do something when these change, instead, they are just functions that are executed at given times in the React lifecycle, evaluating the current context.
Basically, whether a Row should be visible or not depends on two conditions:
allowHover.current set to true?isHovered set to true?Since these don't depend on each other, we should ideally like to be able to modify the conditional content from event listeners attached to both of the events which change the values of these properties.
In a vanilla Javascript environment, we would perhaps store each element depending on this in an array and set its display or visibility from the event listeners which would check both of these conditions; whichever event listener that fires last would be responsible for showing or hiding the component / row.
Doing the same in React, but bypassing React, should be quite straightforward as long as you can store this state in some ref. Since both events occurring on Table level and Row level have to be able to modify the elements in question, access to these DOM elements must be available in both of these components. You can accomplish this by either merging the code of Table, Row and AllRows into one component or pass refs from the children back up to the parent component in some elaborate scheme. The point here is, if you want to do it outside of React, ALL of this should be done outside of React.
Your current problem in the code is that you want to update one of the conditions (allowHover) outside of React but you want React to take care of the other condition (isHovered). This creates an odd situation which is not advisable no matter if you would really want to do this outside of React (which I advise against in all cases except toy scenarios) or not. React does not know when allowHover is set to true since this is done outside of React.
Simply use useState for the allowHover so that Table rerenders whenever allowHover changes. This will update the prop in the children which will rerender too. Also make sure to store the timeout in a ref so that you may clear it whenever you move the mouse in and out of the table.
With this solution, the Table and all its children will rerender whenever the mouse passes in and out of the table (after 1 s.) and then individual Rows will rerender whenever isHovered for that Row is changed. The result is that Rows will rerender on both the conditions which control whether they should contain the conditional content or not.
function Table() {
const [allowHover, setAllowHover] = useState(false);
const [currentRow, setCurrentRow] = useState(null);
const hoverTimeout = useRef(null);
const onHover = (id) => setCurrentRow(id);
const onMouseEnter = (e) => {
if (hoverTimeout.current !== null) clearTimeout(hoverTimeout.current);
hoverTimeout.current = setTimeout(() => {
console.log("Enabling hover");
setAllowHover(true); // allow hovering
}, 1000);
};
const onMouseLeave = (e) => {
if (hoverTimeout.current !== null) clearTimeout(hoverTimeout.current);
hoverTimeout.current = setTimeout(() => {
console.log("Disabling hover");
setAllowHover(false); // dont allow hovering
setCurrentRow(null);
}, 1000);
};
console.log("Rendering table");
return (
<div className="App" style={{ border: "3px solid blue" }}>
<h1>table</h1>
{/* allow/disallow hovering when entering and exiting the table, with a delay */}
<table
style={{ border: "3px solid red" }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<tbody>
<Rows onHover={onHover} currentRow={allowHover ? currentRow : null} />
</tbody>
</table>
</div>
);
}
function Rows(props) {
console.log("Rendering rows");
return [1, 2, 3].map((id) => (
<Row
id={id}
key={id}
isActive={props.currentRow === id}
onHover={() => props.onHover(id)}
/>
));
}
function Row(props) {
console.log("Rendering row");
return (
<tr key={props.id} onMouseEnter={props.onHover}>
<td style={{ border: "1px solid red" }}>---------------- {props.id}</td>
<td style={{ border: "1px solid green" }}>
{props.isActive ? <button>ACTIONS</button> : null}
</td>
</tr>
);
}
Run Code Online (Sandbox Code Playgroud)
No funny business going on here, just plain React style.
Code sandbox: https://codesandbox.io/s/bypass-react-1a-i3dq8
Even though not recommended, if you do this, you should do all the updates outside of React. This means you can't depend on React to rerender child rows when you update the state of the Table outside of React. You could do this in many ways, but one way is for child Rows to pass their refs back up to the Table component which manually updates the Rows via refs. This is pretty much what React does under the hood actually.
Here, we add a lot of logic to the Table component which becomes more complicated but instead, the Row components lose some code:
function Table() {
const allowHover = useRef(false);
const timeout = useRef(null);
const rows = useRef({});
const currentRow = useRef(null);
const onAddRow = (row, id) => {
rows.current = {
...rows.current,
[id]: row
};
onUpdate();
};
const onHoverRow = (id) => {
currentRow.current = id.toString();
onUpdate();
};
const onUpdate = () => {
Object.keys(rows.current).forEach((key) => {
if (key === currentRow.current && allowHover.current) {
rows.current[key].innerHTML = "<button>Accept</button>";
} else {
rows.current[key].innerHTML = "";
}
});
};
const onMouseEnter = (e) => {
if (timeout.current !== null) clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
console.log("Enabling hover on table");
allowHover.current = true; // allow hovering
onUpdate();
}, 1000);
};
const onMouseLeave = (e) => {
if (timeout.current !== null) clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
console.log("Disabling hover on table");
allowHover.current = false; // dont allow hovering
currentRow.current = null;
onUpdate();
}, 1000);
};
console.log("Rendering table");
return (
<div className="App" style={{ border: "3px solid blue" }}>
<h1>table</h1>
{/* allow/disallow hovering when entering and exiting the table, with a delay */}
<table onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<tbody>
<Rows onAddRow={onAddRow} onHoverRow={onHoverRow} />
</tbody>
</table>
</div>
);
}
function Rows(props) {
console.log("Rendering rows");
return [1, 2, 3].map((id) => (
<Row
key={id}
id={id}
onAddRow={props.onAddRow}
onHoverRow={() => props.onHoverRow(id)}
/>
));
}
function Row(props) {
console.log("Rendering row");
return (
<tr onMouseEnter={props.onHoverRow} key={props.id}>
<td style={{ border: "1px solid red" }}>---------------- {props.id}</td>
<td
ref={(ref) => props.onAddRow(ref, props.id)}
style={{ border: "1px solid green" }}
></td>
</tr>
);
}
Run Code Online (Sandbox Code Playgroud)
Code sandbox: https://codesandbox.io/s/bypass-react-1b-rsqtq
You can see for yourself in the console that each component only renders once.
Always first implement things inside React the usual way, then use memo, useCallback, useMemo and refs to improve performance where absolutely necessary. Remember that more complicated code also comes at a cost so just because you're saving some rerenderings with React doesn't mean you have arrived at a better solution.
I changed the code so that once a row has been hovered, it is registered as the current hovered row. This row will then not stop being hovered until another row is hovered or until the table has been disabled (1 s after mouse leaves table). A tiny problem here is that we may enter the table but NOT hover a row so that we are inside the table without any active row. This also means that if we leave the table in this state, no row will be "active" since none was active to begin with. This is due to that there is extra room in the table that is not allocated to any row. It works well despite this but it's good to be aware of. Alas, tables are not really layout components and the more particular your layout gets, the more this shows. If this is a problem, I would use flex-box instead or a table without cell spacing and borders. If borders are required, they could be added inside each cell with absolute positioning instead of relying on the table's spacing and borders.
| 归档时间: |
|
| 查看次数: |
4958 次 |
| 最近记录: |