React state not reactive when triggered from outside library - javascript

React beginner here.
My code 👇
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useState, useEffect } from 'react';
import Mousetrap from 'mousetrap';
export default function Home() {
const [count, setCount] = useState(0);
const triggerSomething = () => {
console.log(count);
};
useEffect(() => {
Mousetrap.bind(['ctrl+s', 'command+s'], e => {
e.preventDefault();
triggerSomething();
});
return () => {
Mousetrap.unbind(['ctrl+s', 'command+s']);
};
}, []);
return (
<div className={styles.container}>
<main className={styles.main}>
<h1 className={styles.title}>count: {count}</h1>
<p className={styles.description}>
<button onClick={() => setCount(count + 1)}>increment</button>
<br />
<br />
<button onClick={triggerSomething}>triggerSomething</button>
</p>
</main>
</div>
);
}
I'm having an issue when trying to trigger an event from Mousetrap. The count variable is not reactive when triggered from mousetrap but reactive when triggered from the button with onClick.
To replicate this bug you need to:
click the increment button once
click the triggerSomething button. The console should print out 1 (the current state of count)
push command+s or ctrl+s to trigger the same method. The console prints out 0 (the state of count when the component loaded). That should print 1 (the current state).
What am I doing wrong? What pattern should I use here?
UPDATE:
Stackblitz here

When you change the state, the component is re-rendered, i.e. the function is executed again, but the useState hook returns the updated counter this time. To use this updated value in your MouseTrap, you must create a new handler (and remove the old one). To achieve this, simply remove the dependency array of your useEffect call. It will then use the newly created triggerSomething function.
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useState, useEffect } from 'react';
import Mousetrap from 'mousetrap';
export default function Home() {
const [count, setCount] = useState(0);
const triggerSomething = () => {
console.log(count);
};
useEffect(() => {
Mousetrap.bind(['ctrl+s', 'command+s'], e => {
e.preventDefault();
triggerSomething();
});
return () => {
Mousetrap.unbind(['ctrl+s', 'command+s']);
};
}); // Notice that I removed the dependency array
return (
<div className={styles.container}>
<main className={styles.main}>
<h1 className={styles.title}>count: {count}</h1>
<p className={styles.description}>
<button onClick={() => setCount(count + 1)}>increment</button>
<br />
<br />
<button onClick={triggerSomething}>triggerSomething</button>
</p>
</main>
</div>
);
}

In the triggerSomething method you can use setCount(count => count + 1) and this should work.
The issue you have is when you don't put the triggerSomething as a dep in the useEffect, count would be the same as the initial state which is 0. but when passing a function counting on the previous value to setCount, you'll spare this issue.

Related

Why this setState caused infinite loop?

I created a react component with rendering call details, in component I use useEffect to set the callInfo state, then it caused infinite loop, even I use [] as second parameter, can anyone help me fix this, thanks!
import { useLocation } from "react-router-dom";
import { useState, useEffect } from "react";
const ActivityDetail = ({ onToggleArchived }) => {
const { call } = useLocation().state;
const [callInfo, setCallInfo] = useState(null);
console.log({...call});
useEffect(() => {
setCallInfo({ ...call });
}, [])
return (
<div>
<h3 className="title">Call Details</h3>
<hr />
{
callInfo && <div>
<p>From: {callInfo.from}</p>
<p>To: {callInfo.to}</p>
<p>Time: {callInfo.created_at}</p>
<button onClick={onToggleArchived(callInfo.id)}>
{callInfo.is_archived ? "Unarchive" : "Archive"}
</button>
</div>
}
</div>
)
}
export default ActivityDetail
This is error information:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
The problem lies within your return:
<button onClick={onToggleArchived(callInfo.id)}>
{callInfo.is_archived ? "Unarchive" : "Archive"}
</button>
Here, you are calling the function onToggleArchived which presumably (it's not in the code you posted) does state updates.
how to fix it:
wrap it in an arrow function
<button onClick={()=>onToggleArchived(callInfo.id)}>
{callInfo.is_archived ? "Unarchive" : "Archive"}
</button>
EDIT: In addition to the original answer about misusing state (which you need to correct), I missed the point that you were calling the function instead of wrapping it:
<button onClick={() => onToggleArchived(callInfo.id)}>
// instead of
<button onClick={onToggleArchived(callInfo.id)}>
ORIGINAL ANSWER
in component I use useEffect to set the callInfo state
But this is a problem because call is not component state - it's coming from useLocation(). Just let it come from there and remove the component state stuff altogether.
i.e. treat it as if it were a prop.
import { useLocation } from "react-router-dom";
import { useState, useEffect } from "react";
const ActivityDetail = ({ onToggleArchived }) => {
const { call: callInfo } = useLocation().state;
return (
<div>
<h3 className="title">Call Details</h3>
<hr />
{
callInfo && <div>
<p>From: {callInfo.from}</p>
<p>To: {callInfo.to}</p>
<p>Time: {callInfo.created_at}</p>
<button onClick={() => onToggleArchived(callInfo.id)}>
{callInfo.is_archived ? "Unarchive" : "Archive"}
</button>
</div>
}
</div>
)
}
export default ActivityDetail

How can I append a React component to an html element i?

I am using the modal library winbox. It works well if use simple html and javascript. But I can't append a React node to it.
The modal has a html parameter, that accept an html string such as div.innerHTML = <div>hello</div> . The code source is: this.body.innerHTML = html;.
So adding a classic React element makes the modal crash. The only solution I found is to use react-dom's renderToString method: html: renderToString(children). But the component is not dynamic anymore (no state update, etc.).
I also tried to surround React.renderDOM by a div inside the modal and attach the component to it, as the app is attached to index.js's #root div.
html: <div id="modal">
{render(
<div children={content} />,
document.querySelector("#modal")
)},
</div>
My question is thus: how to pass a dynamic React component to the body of this modal?
Here is my code:
import React from "react";
import useModal from "./useModal";
const Counter = () => {
const [count, setCount] = useState(1);
return (
<div>
The count is {count}
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
};
export default function App() {
const [open, setOpen] = useState(false);
useModal(open, setOpen, <Counter />);
return (
<div id="#hi">
<button onClick={() => setOpen(!open)}>toggle</button>
</div>
);
}
// useModal
import WinBox from "winbox/src/js/winbox.js";
import "winbox/dist/css/winbox.min.css";
import React from "react";
import { render } from "react-dom";
export default function useModal(open, onToggle, content) {
const modal = open
? new WinBox({
title: "Hello",
max: true,
html: content, // INSERT DYNAMIC COMPONENT HERE
onclose: () => onToggle(false)
})
: null;
return modal;
}
Thank you!
You can use winbox-react package from npm. winbox-react npm link Example code is given below. Youcan use normal jsx as children of the WinboxReact component.
const Hero = () => {
const [show, setShow] = useState(false)
const clickHandeler = () => {
setShow(!show)
}
return (
<div className='text-center'>
{show && (
<WinboxReact
onClose={clickHandeler}
background='linear-gradient(90deg, rgba(49,36,239,1) 0%, rgba(67,0,168,1) 100%)'
><h1>Hello</h1></WinboxReact>
)}
<button onClick={clickHandeler} className='btn btn-custom btn-lg mt-4'>
Show Example
</button>
</div>
)
}
export default Hero

React: how to change the state if something outside the react app changes?

Remark: this is not a duplicate of Change the state when clicking outside a component in React
I do not want to handel mousedown/mouseup events.
I would like to do change the state of my component "from the outside". So the component will wait until some action id performed by the HTML code (button click).
The click can change something in the DOM, but is there a way (another then watching for buttondown events) to detect the change?
The result will be like this:
<div id="root">
... the component will be rendered here, hidden at the beginning
</div>
<button onClick=....>Show the Component</button>
You have at least a couple of options:
Use a portal, either to move the button into the React tree even though it's elsewhere in the DOM, or to render your desired component elsewhere. But if you want to keep the button entirely outside of React, this option doesn't apply.
If you want to leave the button entirely outside of React, use a parent component around the component you want rendered/not-rendered, and have that parent component listen for clicks on the button element. Hook up that click handler in componentDidMount or a useEffect hook with no dependencies, and remove it in componentWillUnmount or a cleanup callback you return from your useEffect.
Or driving it from the other side, use non-React code to hook the click and then respond to the click by mounting the component directly on id="root" via ReactDOM.render.
Here's an example of #2:
const {useState, useEffect} = React;
const TheComponent = () => {
return <div>This is the component</div>;
};
const Example = () => {
const [showing, setShowing] = useState(false);
useEffect(
() => {
const clickHandler = () => {
setShowing(flag => !flag);
};
console.log("Hooking click on button");
document.getElementById("the-button").addEventListener("click", clickHandler);
// return cleanup handler:
return () => {
console.log("Unhooking click on button");
document.getElementById("the-button").removeEventListener("click", clickHandler);
};
},
[] // <== No deps = mount/unmount only
);
return (
<div>
{showing && <TheComponent />}
</div>
);
};
const App = () => {
const [mounted, setMounted] = useState(true);
return (
<div>
{mounted && <Example />}
<div>
<input type="button" value="Unmount the parent component entirely" onClick={() => setMounted(false)} />
</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<div>React <code>id="root"</code>:</div>
<div id="root"></div>
<div style="margin-top: 10px">Outside of React:</div>
<input type="button" id="the-button" value="Toggle">
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>
Here's an example of #3:
const {useState, useEffect} = React;
const TheComponent = () => {
return <div>This is the component</div>;
};
const root = document.getElementById("root");
let mounted = false;
document.getElementById("the-button").addEventListener("click", () => {
if (mounted) {
ReactDOM.unmountComponentAtNode(root);
} else {
ReactDOM.render(<TheComponent />, root);
}
mounted = !mounted;
});
<div>React <code>id="root"</code>:</div>
<div id="root"></div>
<div style="margin-top: 10px">Outside of React:</div>
<input type="button" id="the-button" value="Toggle">
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

ReactJs: Best Practice of defining event handlers

I recently started using ReactJs and I want to know what is the best practice of defining event handlers in React. This is how I've been using react event handlers:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [counter, setCounter] = useState(0);
const handleButtonClick = () => {
setCounter(counter + 1);
};
return (
<div className="App">
<button onClick={() => handleButtonClick()}>
Click Me To increase counter
</button>
<div>
<h4>Counter value is: </h4>
{counter}
</div>
</div>
);
}
I have also heard arguments against this logic. Some say it is better to define event handlers outside the definition of the component (App in our case). This way, it becomes clear, clean and concise, instead of creating a mess of multiple functions (event handlers or not) inside the component. For example:
import React, { useState } from "react";
import "./styles.css";
const handleButtonClick = (setCounter, counter) => () => {
setCounter(counter+1);
};
export default function App() {
const [counter, setCounter] = useState(0);
return (
<div className="App">
<button onClick={handleButtonClick(setCounter, counter)}>
Click Me To increase counter
</button>
<div>
<h4>Counter value is: </h4>
{counter}
</div>
</div>
);
}
CodeSandbox Link for second approach
I want to know which is the best practice of defining functions? Should event handlers be also defined globally above the function component(App Component in this case)?
You don't need to create an extra function inside onClick. Just don't call it. onClick method call when onClick trigger.
const handleButtonClick = () => {
setCounter(counter + 1);
}; // return function
<div onClick={handleButtonClick} />
// it will be call the handleButtonClick
// when onClick is trigger
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [counter, setCounter] = useState(0);
const handleButtonClick = () => {
setCounter(counter + 1);
};
return (
<div className="App">
<button onClick={handleButtonClick}>
Click Me To increase counter
</button>
<div>
<h4>Counter value is: </h4>
{counter}
</div>
</div>
);
}
Should event handlers be also defined globally above the function component?
NO.
Defining event handlers, outside the component, that modifies a component's state breaks cohesion.
An event handler, within a component, explains the different interactions of the component. Having a "wrapper" handler simply breaks the single level of abstraction.
My two cents.

Enzyme wrapper.update() causes ref input to no longer have value prop

Here is a Code Sandbox that contains a test simulating this issue. The test in this Code Sandbox fails as described in this question: https://codesandbox.io/s/react-jest-and-enzyme-testing-c7vng
I'm trying to test the value of an <input /> that gets updated inside a useEffect. This is the code from the Code Sandbox, which is a simplified version of something I'm trying to do in a project.
import React, { useEffect, useRef, useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
ref.current.value = "";
console.log(typeof ref.current.value);
}, [count]);
const ref = useRef(null);
const handleClick = () => {
setCount(count + 1);
console.log(count);
};
return (
<div>
<input ref={ref} type="text" />
<button onClick={handleClick}>click me</button>
</div>
);
};
export default App;
I use useRef to set the value of the <input />.
The useEffect gets called when the <button /> is clicked. The <button /> updates the useState count. useEffect is watching updates to count, and it gets called as a side-effect.
In the useEffect, I set ref.current.value to an empty string, and then I log the typeof this value to verify that it's a string.
In the test, I try to simulate this behavior:
describe("App", () => {
const wrapper = mount(<App />);
wrapper.find("input").props().value = "hello";
act(() =>
wrapper
.find("button")
.props()
.onClick()
);
console.log(wrapper.find("input").debug());
wrapper.update();
console.log(wrapper.find("input").debug());
expect(wrapper.find("input").length).toBe(1);
expect(wrapper.find("input").props().value).toBe("hello");
});
I set the value prop to 'hello'. I then invoke the <button /> onClick prop, effectively clicking it. I then call wrapper.update(), and I also debug() log the <input /> before and after update().
Before, update(), the <input /> has a value prop containing 'hello'. After the update(), the <input /> does not have a value prop. This causes the test to fail, saying that the <input /> value is undefined after the update.
Shouldn't the input's value be '' after the update?
Here is a list of issues with the current way:
<input ref={ref} type="text" /> is describes a React Element and has no value prop
Prop value should be controlled via state and not mutated directly
wrapper.find("input").props().value = "hello";
Setting value on a DOM Node isn't the same as setting a prop value. Using React means that you ceed DOM manipulation to it.
useRef allows for access to the underlying DOM Node when passed an initial value of null and this following line mutates DOM in spite of App state.
ref.current.value = "";
In certain scenarios, it's expedient to manipulate DOM in spite of the App state. The tests should then deal with the DOM Node and check changes to it.
describe("App", () => {
const wrapper = mount(<App />);
wrapper.find("input").getDOMNode().value = "hello";
act(() =>
wrapper
.find("button")
.props()
.onClick()
);
wrapper.update();
expect(wrapper.find("input").length).toBe(1);
expect(wrapper.find("input").getDOMNode().value).toBe("");
});
If you consider that your use case doesn't require this much of a direct control of the DOMNode.
The input element value prop can be controlled with state. For example,
const App = () => {
const [count, setCount] = useState(0);
const [value, setValue] = useState("hello");
useEffect(() => {
setValue("");
}, [count]);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<input type="text" value={value} />
<button onClick={handleClick}>click me</button>
</div>
);
};
export default App;

Categories

Resources