React State not updating properly - javascript

I'm building a text editor using React with Typescript.
The component hierarchy looks like this: TextEditor -> Blocks -> Block -> ContentEditable.
The ContentEditable is an npm package https://www.npmjs.com/package/react-contenteditable.
What i want it to do
The behavior I'm after is similar to Medium or Notions text editor. When a user writes in a block and hits enter on their keyboard, a new block should be created after the current block.
What it does
The behavior right now is strange to me. If I press enter and add one block, it works fine. But if I press enter again it overrides the previous block instead of creating a new one.
However, if I press enter and add a block, then puts the carrot (focusing) on the new block and press enter again, a new block is added after as expected.
Sandbox
Here is a sandbox with the complete code: https://codesandbox.io/s/texteditor-mxgbey?file=/src/components/Block.tsx:81-557
TextEditor
export default function TextEditor(props) {
const [blocks, setBlocks] = useState([
{ id: "1", tag: "h1", html: "Title1" },
{ id: "2", tag: "p", html: "Some text" }
]);
function handleAddBlock(id: string) {
const index = blocks.findIndex((b) => b.id === id);
let copiedBlocks = [...blocks];
let newBlock = { id: nanoid(), tag: "p", html: "New block..." };
copiedBlocks.splice(index + 1, 0, newBlock);
setBlocks(copiedBlocks);
}
return <Blocks injectedBlocks={blocks} handleAddBlock={handleAddBlock} />;
}
Blocks
export default function Blocks(props) {
const { injectedBlocks, handleAddBlock } = props;
return (
<>
{injectedBlocks.map((b) => {
return (
<Block
key={b.id}
id={b.id}
tag={b.tag}
html={b.html}
handleAddBlock={handleAddBlock}
/>
);
})}
</>
);
}
Block
export default function Block(props) {
const { id, tag, html, handleAddBlock } = props;
function handleChange(e: React.SyntheticEvent) {}
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === "Enter") {
console.log("Enter pressed on: ", id);
e.preventDefault();
handleAddBlock(id);
}
}
return (
<ContentEditable
tagName={tag}
html={html}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
);
}

State value not give the updated value while handleAddBlock function calls.
So use like this,
setBlocks((p) => {
let copiedBlocks = [...p];
let newBlock = { id: nanoid(), tag: "p", html: "New block..." };
copiedBlocks.splice(index + 1, 0, newBlock);
return copiedBlocks;
});
This will gives the updated state value immediately.

Related

React/Jest Select element onChange values not updating

I have the following code to test the value of the Select element after changing:
it("changes value after selecting another field", () => {
doSetupWork();
let field = screen.getByLabelText("MySelectField");
expect(field).toHaveValue("");
fireEvent.change(field, { target: { value: "1" } });
// Insert one of two options from below
});
However, when I insert the following at the bottom, it does not work:
field = screen.getByLabelText("MySelectField");
expect(field).toHaveValue("1");
and gives the following error message:
Expected the element to have value: 1
Received:
But, when I wrap it in a setTimeout with just 1ms delay, it does work:
setTimeout(() => {
field = screen.getByLabelText("MySelectField");
expect(field).toHaveValue("1");
}, 1);
It feels like there should be a more elegant way of writing this without setTimeout. Any advice?
When I am using react-testing-library I tend to use render when I have events to interact with.
For instance:
In my App.js I have this code on the return method
const handleChoice = () => {};
const attributes = [
{ label: "One", value: "1" },
{ label: "Two", value: "2" },
{ label: "Three", value: "3" }
];
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<select onChange={handleChoice} data-testid="MySelectField">
<option value="0">Zero</option>
{attributes.map((item) => {
return (
<option key={item.value} value={item.value}>
{item.label}
</option>
);
})}
</select>
</div>
);
then my test would be something like this:
import { fireEvent, render } from "#testing-library/react";
import "#testing-library/jest-dom";
import App from "./App";
it("changes value after selecting another field", () => {
const { getByTestId } = render(<App />);
let field = getByTestId("MySelectField");
expect(field).toHaveValue("0");
fireEvent.change(field, { target: { value: "1" } });
expect(field.value).toBe("1");
fireEvent.change(field, { target: { value: "3" } });
expect(field.value).toBe("3");
// Insert one of two options from below
});
Take a look in this sandbox to see it working.
https://codesandbox.io/s/ecstatic-https-hzi5n?file=/src/App.spec.js
You could try using waitFor instead of setTimeout:
import {waitFor} from '#testing-library/react'
...
await waitFor(() => screen.getByLabelText("MySelectField").toHaveValue("1"))
What you're seeing is that it takes a finite amount of time for your app to react (excuse the pun) to the change that has occurred - specifically, it needs to re-render.
And yes, there is a nicer way - the waitFor function from testing-library/react:
import { screen, waitFor } from '#testing-library/react';
...
it("changes value after selecting another field", async () => {
...
fireEvent.change(field, { target: { value: "1" } });
await waitFor(async () => {
field = screen.getByLabelText("MySelectField");
expect(field).toHaveValue("1");
});
}
Note that the entire test body (i.e. after the test's name) has to be declared async in order to be able to await the new waitFor block.

Set a dropdown's value in React Testing Library

For testing an input it works like this:
it('test input', () => {
const { getByTestId, getByLabelText } = render(<MyComponent />);
const myButton = getByTestId('submit-button');
expect(myButton).toBeInTheDocument();
fireEvent.change(getByLabelText('first-name'), {
target: { value: 'ted' }
});
// do something
});
the above test is setting the value of first-name input to "ted" and moves forward.
I want to do something similar but for a drop-down selector.
This is the component:
<SelectInput
label='my-dropdown'
onChange={onChange}
options={myOptions}
/>;
myOptions is an array of this shape:
const myOptions = [
{ id: '0', name: 'zero' },
{ id: '1', name: 'one' },
{ id: '2', name: 'two' }
];
it works fine in the application, no errors from this part.
Here comes the testing of it, I did something but it doesn't work:
it('test dropdpwn', () => {
const { getByTestId, getByLabelText } = render(<MyComponent />);
const saveButton = getByTestId('submit-button');
expect(saveButton).toBeInTheDocument();
fireEvent.change(getByLabelText('my-dropdown'), {
target: { value: { id: '0', name: 'zero' } }
});
// do something
});
the above code doesn't work, it doesn't set the dropdown with that value.
Any ideas on how to solve this? Not sure if it's important, but all those inputs are inside a react-hook-form and at the end it should test that the onSubmit is working (it works only if all inputs are set).
So, since the select behavior is being achieved using a button and spans.
You need to first click the button this would bring all the options on the screen and then you need to click one of those options.
And then you can finally test that the selected option is now on the screen.
it("test dropdpwn", async () => {
const { getByTestId, getByLabelText } = renderWithClientInstance(
<MapSignalModal title={title} open={true} toggle={toggle} />
);
userEvent.click(screen.getAllByTestId("selectButton")[0]);
userEvent.click(screen.getByText("sensor pool 1"));
expect(
await screen.findByText(screen.getByText("sensor pool 1"))
).toBeInTheDocument();
});
Also, to be really sure you can try the following, this should fail because "sensor pool 1" option is not initially on the screen.
And it should pass when the text is changed to "sensor pool 0" because that's there on the screen initially.
it("test dropdpwn", async () => {
const { getByTestId, getByLabelText } = renderWithClientInstance(
<MapSignalModal title={title} open={true} toggle={toggle} />
);
expect(screen.getByText("sensor pool 1")).toBeInTheDocument();
// if you replace the above text to "sensor pool 0", it should work
});

Problem with alert after adding new value to the array in React.js

During the React.js course I'm doing, I was tasked with making a simple fortune-teller app. Theoretically, everything works as planned, but I did the task differently than the tutor. Instead of a simple fortune-telling table, I've created an array of objects, each with its id and 'omen'. The problem arose when after adding a new 'omen' an alert should be displayed that gives the current content of 'omens' in state. Only the previous values appear, without the added value. I will be grateful for the hints. In the original design, this problem does not occur, although it is very similar.
class Draw extends React.Component {
state = {
index: "",
value: "",
omens: [
{ id: 1, omen: "Hard work pays off" },
{ id: 2, omen: "You will be rich" },
{ id: 3, omen: "Be kind to others" },
],
};
handleDrawOmen = () => {
const index = Math.floor(Math.random() * this.state.omens.length + 1);
this.setState({
index: index,
});
};
showOmen = () => {
let omens = this.state.omens;
omens = omens.filter((omen) => omen.id === this.state.index);
return omens.map((omen) => (
<h1 id={omen.id} key={omen.id}>
{omen.omen}
</h1>
));
};
handleInputChange = (e) => {
this.setState({
value: e.target.value,
});
};
handleAddOmen = () => {
if (this.state.value === "") {
return alert("Enter some omen!");
}
const omens = this.state.omens.concat({
id: this.state.omens.length + 1,
omen: this.state.value,
});
this.setState({
omens,
value: "",
});
console.log(this.state.omens);
alert(
`Omen added. Actual omens: ${this.state.omens.map(
(omen) => omen.omen
)}`
);
};
render() {
return (
<div>
<button onClick={this.handleDrawOmen}>Show omen</button>
<br />
<input
placeholder="Write your own omen..."
value={this.state.value}
onChange={this.handleInputChange}
/>
<button onClick={this.handleAddOmen}>Add omen</button>
{this.showOmen()}
</div>
);
}
}
ReactDOM.render(<Draw />, document.getElementById("root"));
The state object is immutable. So you need to create your new array and apply it afterwards:
const omens = [
...this.state.omens,
{
id: this.state.omens.length + 1,
omen: this.state.value,
}
]
also setState is async so you need to wait until it finished:
this.setState({
omens,
value: "",
}, () => {
alert(
`Omen added. Actual omens: ${this.state.omens.map(
(omen) => omen.omen
)}`
)
https://reactjs.org/docs/react-component.html#setstate

Rendering the navigation list from an array based on different label on toggle mode

I have a header component where I need to render three buttons, so every three buttons have three props. One is the class name, click handler and text.
So out of three buttons, two buttons act as a toggle button, so based on the click the text should change.
See the below code:
class App extends Component(){
state = {
navigationList: [{
text: 'Signout',
onClickHandler: this.signoutHandler,
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode,
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay,
customClassName: 'buttonStyle'
}]
}
signoutHandler = () => {
// some functionality
}
viewMode = () => {
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay = () => {
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<Header navigationList={this.state.navigationList}/>
)
}
}
const Header = ({navigationList}) => {
return (
<>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</>
)
}
The other way is I can pass all the props one by one and instead of an array I can write three button elements render it, but I am thinking to have an array and render using a map.
So which method is better, the problem that I am facing is if use the array. map render
the approach I need to set the initial value as a variable outside and how can I set the state.
And I am getting the onClick method is undefined, is it because the function is not attached to the state navigation list array.
Update
I declared the functions above the state so it was able to call the function.
So in JS, before the state is declared in the memory the functions should be hoisted isn't.
class App extends React.Component {
constructor(props){
super();
this.state = {
isStudents:false,
activeWay:false,
}
}
createList(){
return [{
text: 'Signout',
onClickHandler: this.signoutHandler.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay.bind(this),
customClassName: 'buttonStyle'
}];
}
signoutHandler(){
}
viewMode(){
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay(){
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<div>
<div>ddd</div>
<Header navigationList={this.createList()} />
</div>
)
}
}
const Header = ({navigationList}) => {
console.log(navigationList);
return (
<div>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#app"))
https://jsfiddle.net/luk17/en9h1bpr/
Ok I will try to explain, If you see you are using function expressions in your class and as far as hoisting is concerned in JavaScript, functions expressions are not hoisted in JS only function declarations are hoisted, function expressions are treated as variables in JS.
Now for your case you don't have to shift your functions above the state, you can simply use constructor for initializing state as
constructor(props) {
super(props);
this.state = {
isStudents: false,
activeWay: false,
navigationList: [
{
text: "Signout",
onClickHandler: this.signoutHandler,
customClassName: "buttonStyle"
},
{
text: "Teachers",
onClickHandler: this.viewMode,
customClassName: "buttonStyle"
},
{
text: "Active Hidden",
onClickHandler: this.activeWay,
customClassName: "buttonStyle"
}
]
};
}
Now you will have your handlers available as it is
Sandbox with some modification just to show
EDIT:
You can have default text for buttons and change it when clicking,
Sandbox updated
Hope it helps

Can't update state property? React

I am attempting to validate an input manually by passing up the chain the input of a text field. Some magic is supposed to happen whereby the following conditions are checked:
if input text matches that already held in an array - error = "already exists" & the text isn't added to the list
if input text is blank - error = "no text input" & the text isn't added to the list
if input text is not blank and does not already exist - run another method to add text to the list
The error is set to null by default
Currently in the input.js file, the {this.props.renderError} line causes an "underfined" output in the console before anything happens. I understand why this occurs, but I wondered if there was any way to stop it?
Functionality-wise: I can get the error message to output, however this appears to run after the text is already placed in the list of tasks...
Checkout the sandbox for this code
App.js (parent)
const tasks = [
{ name: 'task1', isComplete: false },
{ name: 'task2', isComplete: true },
{ name: 'task3', isComplete: false },
]
class App extends React.Component {
constructor() {
super();
this.state = {
error: null,
}
}
render() {
return (
<div>
<Input
createTask={this.createTask.bind(this)}
renderError={this.renderError.bind(this)}
taskList={this.state.tasks}
throwError={this.throwError.bind(this)}
/>
</div>
)
}
createTask(task, errorMsg) {
this.throwError(errorMsg);
if (this.state.error) {
return;
} else {
this.setState((prevState) => {
prevState.tasks.push({ name: task, isComplete: false });
return {
tasks: prevState.tasks
}
})
}
}
throwError(errorMsg) {
if (errorMsg) {
this.setState((prevState) => {
prevState.error = errorMsg;
return {
error: prevState.error
}
})
}
return;
}
renderError() {
if (this.state.error) {
return <div style={{ color: 'red' }}>{this.state.error}</div>
}
}
Input.js (child)
render() {
return (
<form ref="inputForm" onSubmit={this.handleCreate.bind(this)}>
<TextField placeholder="Input.js"/>
<Button type="submit">Click me</Button>
{this.props.renderError()}
</form>
)
}
validateInput(taskName) {
if (!taskName) {
return '*No task entered';
} else if (this.props.taskList.find(todo => todo.name.toLowerCase() === taskName.toLowerCase())) {
return '*Task already exists'
} else {
return null;
}
}
handleCreate(event) {
event.preventDefault();
// Determine task entered
var newTask = this.refs.inputForm[0].value;
// Constant for error message returned
const validInput = this.validateInput(newTask);
// If error message produced - trigger error to be shown & end
if (newTask) {
this.props.createTask(newTask, validInput);
this.refs.inputForm.reset();
}
}
Update
I have since found that I can make this work if I move the renderError and throwError methods to input.js and also transfer across the state property error.

Categories