How to avoid re-rendering with react-swipe library and useState - reactjs

I am using a react library for swiping: https://github.com/voronianski/react-swipe
Making it work is straight-forward:
import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom';
import ReactSwipe from 'react-swipe'
import styled from 'styled-components'
const StyledBox = styled.div`
height: 50px;
width: 100%;
background-color: orange;
`
const Carousel = () => {
let reactSwipeEl;
const [position, setPosition] = useState(0)
console.log('position', position)
const swipeOptions = {
continuous: false,
transitionEnd() {
setPosition(reactSwipeEl.getPos())
}
}
return (
<div>
<ReactSwipe
className="carousel"
swipeOptions={swipeOptions}
ref={el => (reactSwipeEl = el)}
>
<StyledBox>PANE 1</StyledBox>
<StyledBox>PANE 2</StyledBox>
<StyledBox>PANE 3</StyledBox>
</ReactSwipe>
<Circles>
<Circle isActive={0 === position} />
<Circle isActive={1 === position}/>
</Circles>
</div>
);
};
export default Carousel
The code that I added is the one related with useState. The library works fine, but every time I swipe, I use the callback transitionEnd, in order to update the position state. It is updated, but since a state variable changes, the whole Carousel component gets updated, setting automatically the init value, which is 0.
I don't understand how can I avoid this problem which is, updating the state without re-rendering the whole component. Should I split it in two components and use a provider?
I tried it, but when the state of the provider changes, both components are also re-render

The swipeOptions is recreated on each render, which causes the ReactSwipe to rerender as well. Wrap the swipeOptions with useMemo() (sandbox);
const swipeOptions = useMemo(() => ({
continuous: false,
transitionEnd() {
setPosition(reactSwipeEl.current.getPos())
}
}), []);

Related

React using Typewriter effect, re render onInit issue?

So I am working a simple text adventure game using Rails/React.
I am attempting to use the following package: https://www.npmjs.com/package/typewriter-effect
The current functionality has it rendering my dialogue text for the individual storylines.
When I navigate to a new storyline (via a choice) my state updates and re render occurs. But it doesn't re render the new Typewriter effect and associated dialogue.
I have an allTypeWriters variable that creates instance for each storyline. Then I find the relative one based on the newly updated storyLineId.
When I go to the character homepage (navigate away) and then return to the storyline route it will begin the effect with the correctly associated dialogue. If I simply re render via the choice picked, the img tag associated re renders. The choices available re renders. My redux and localStorage all update. Yet the dialogue implemented by the typewriter effect itself stays the same.
I'm assuming it has something to do with the onInit function that belongs to the Typewriter class and I need to recall that? And possibly even remove the previous element after re render.
Any ideas or has anyone worked with this or a similar package in the past, thanks in advance!
import React, { useEffect } from "react";
import "../Dialogue/Dialogue.css";
// import { useEffect, useCallback } from "react";
import Typewriter from "typewriter-effect";
// import { useState } from "react";
// import { useNavigate } from "react-router-dom";
function Dialogue({ storyLine }) {
// const [localWriter, setLocalWriter] = useState({});
const allTypeWriters = JSON.parse(localStorage.getItem("stories")).map(
(story) => {
return (
<Typewriter
options={{
delay: 10,
}}
onInit={(typewriter) => {
typewriter.typeString(story.dialogue).start();
}}
/>
);
}
);
const storyLineId = JSON.parse(
localStorage.getItem("user_data")
).current_storyline;
const returnActive = () => {
const activeType = allTypeWriters.find(() => storyLineId);
return activeType;
};
useEffect(() => {
returnActive();
}, [storyLineId]);
return (
<div className="dialogue-container">
{returnActive()}
<p className="dialogue-img">{storyLine.storyline_img}</p>
</div>
);
}
export default Dialogue;

How to use useCallback hook with onChange function REACT

I have simple React component: How I can improve this component by adding useCallback hook?
Is it even a good idea to add useCallback hook in this component? If yes, to which function should I add it.
import styled from "styled-components"
import React, { useState } from "react"
import { useGetFilteredBooks } from "../../hooks/useGetFilteredBooks"
import { useRecoilState } from "recoil"
import { Books, PageNumber, SearchText } from "../../recoil/globalState"
export const SearchBook = () => {
const [text, setText] = useRecoilState(SearchText)
const [pageNumber, setPageNumber] = useRecoilState(PageNumber)
const [setBooks] = useRecoilState(Books);
const { refetch } = useGetFilteredBooks(text, setBooks, setPageNumber);
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setText(event.target.value)
}
const handleOnSubmit = (e: any) => {
e.preventDefault();
refetch();
}
return (
<>
<form onSubmit={handleOnSubmit}>
<Input value={text} onChange={handleOnChange} placeholder="Enter the name of the book or author" />
<button type="submit">Show</button>
</form>
</>
)
}
const Input = styled.input`
padding: 10px 10px;
width: 300px;
`
It is a good idea to memoize a function in useCallback if you know that you will be going to use those functions as a dependency in other hooks like useEffect or useMemo, in your case, your functions are pretty trivial and you are not using them as deps to other hooks, the computation cost of recreating them on each re-render is negligible hence the usage of useCallback, specifically, here is not needed. Just be aware that if you put other non memoized functions in the deps array of a useCallback, that will be worth nothing. If you still plan to wrap those functions in useCallback to use them in other components / hooks:
import styled from "styled-components"
import React, { useState, useCallback } from "react"
import { useGetFilteredBooks } from "../../hooks/useGetFilteredBooks"
import { useRecoilState } from "recoil"
import { Books, PageNumber, SearchText } from "../../recoil/globalState"
export const SearchBook = () => {
const [text, setText] = useRecoilState(SearchText)
const [pageNumber, setPageNumber] = useRecoilState(PageNumber)
const [setBooks] = useRecoilState(Books);
const { refetch } = useGetFilteredBooks(text, setBooks, setPageNumber);
const handleOnChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setText(event.target.value)
},[]) // No deps needed here according to ESLint, since react setState<s> do not change during re-renders.
const handleOnSubmit = useCallback((e: any) => {
e.preventDefault();
refetch();
},[refetch]) // refetch is needed here, since it might change during re-renders, you should make sure to memoize that function as well
return (
<>
<form onSubmit={handleOnSubmit}>
<Input value={text} onChange={handleOnChange} placeholder="Enter the name of the book or author" />
<button type="submit">Show</button>
</form>
</>
)
}
const Input = styled.input`
padding: 10px 10px;
width: 300px;
`
What does useCallback do?
Per the docs:
useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed.
So, memoized callbacks are essentially functions that survive rerender cycles.
Why would we need memoized callbacks?
Per the docs:
This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
Combining memoized callbacks with components that rely on reference equality, useEffect is an example in functional components, avoids rerendering these child components unnecessarily.
Are you using components that rely on reference equality?
No. To make your application just "work", at the moment you don't need useCallback.
Should you still apply useCallback for performance?
One might think, useCallback might enable better performance since functions aren't recreated. This has been discussed in an article by Kent C. Doods or in this reddit. The bottom line is that premature, unscientific optimization is a waste of time, and potentially harmful. Refrain from applying memoization if you're application works and is fast enough.

react hook useState in AgGridColumn onCellClicked function

Currently, I am using functional react components with react hook useState and an AgGridReact Component.
I am displaying an AgGridReact and put a onCellClicked function on a AgGridColumn. So far everything is working. In the onCellClicked function I want to update my state and do something depending on its current value.
Here is the problem:
if I want to use my state get/set (useState hook) inside of the onCellClicked function it is not working as expected. For some reason, I can not update my state.
In a react class component it is working.
EDIT: I experimented for a while and found out that in the onCellClicked function I have only the default value in myText. I can update it once. If I spam the onCellClicked function it will append the text again to the default value from useState("default myText");. I would expect that the string would get longer as often I click on the cell. Just as in my Class Component example.
If I use a simple button outside of the AgGridReact <button onClick={() => setMyText(myText + ", test ")}>add something to myText state</button> it is working as expected, the string gets longer every time I click on my <button>. If I change the state of myText via the <button> outside of the AgGridReact and then click on the cell function again the state previously setted through my <button> is lost.
Example react hook component:
import React, { useState } from 'react';
import { AgGridColumn, AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
function App() {
const [myText, setMyText] = useState("default myText");
const [todoListRowData, setTodoListRowData] = useState([]);
// ....fetch data and set the todoListRowData state....
const myCellClickFunction = (params, x) => {
// here is the problem:
// no matter how often I click in the cell myText is every time the default value 'default myText'
// EDIT: I found out I can update the state here but only from the initial default value once, myText is on every cell click again "default myText" and will be concatenated with "hookCellClicked". So every time I click this cell the state gets again "default myTexthookCellClicked"
console.log(myText);
setMyText(myText + "hookCellClicked");
}
return (
<div className="ag-theme-alpine" style={{ height: '600px', width: '100%' }}>
<AgGridReact rowData={todoListRowData} >
<AgGridColumn headerName="ID" field="id" maxWidth="50"></AgGridColumn>
<AgGridColumn headerName="UserId" field="userId" maxWidth="85"></AgGridColumn>
<AgGridColumn headerName="Title" field="title" minWidth="555"></AgGridColumn>
<AgGridColumn headerName="completed" field="completed"></AgGridColumn>
<AgGridColumn headerName="Testcol" onCellClicked={(params) => myCellClickFunction(params)}></AgGridColumn>
</AgGridReact>
</div>
}
export default App;
If I do the exact same thing in a class component it is working fine.
Example Class Component:
import React from 'react';
import { AgGridColumn, AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
class MyClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
myClassComponentRowData: [],
testState: "defaul testState"
};
}
// ....fetch data and set ag grid rowData state....
handleCellClick = (params) => {
// here everything works just fine and as expected
// every time I click on the cell the state testState updates and it is added ", handleCellClick" every time
console.log(this.state.testState);
this.setState({testState: this.state.testState + ", handleCellClick"});
}
render() {
return <div className="ag-theme-alpine" style={{ height: '600px', width: '100%' }}>
<AgGridReact rowData={this.state.myClassComponentRowData} >
<AgGridColumn headerName="ID" field="id" maxWidth="50"></AgGridColumn>
<AgGridColumn headerName="UserId" field="userId" maxWidth="85"></AgGridColumn>
<AgGridColumn headerName="Title" field="title" minWidth="555"></AgGridColumn>
<AgGridColumn headerName="completed" field="completed"></AgGridColumn>
<AgGridColumn headerName="Testcol" onCellClicked={(params) => this.handleCellClick(params)}></AgGridColumn>
</AgGridReact>
</div>
}
}
export default MyClassComponent;
Am I doing something wrong? I want to use a functional component with react hooks.
There is nothing wrong with the code in your question except that the callback myCellClickFunction references the old state myText which is captured in the previous render call. If you log in the render method, you can see the state is updated properly. This problem is called stale closure.
function App() {
const [myText, setMyText] = useState("default myText");
const [todoListRowData, setTodoListRowData] = useState(rowData);
console.log("render", myText); // prints the latest myText state
...
}
You can see my other answer here about how to get the latest state in callback when using React hook. Here is an example for you to try that out.
function useExtendedState(initialState) {
const [state, setState] = React.useState(initialState);
const getLatestState = () => {
return new Promise((resolve, reject) => {
setState((s) => {
resolve(s);
return s;
});
});
};
return [state, setState, getLatestState];
}
function App() {
const [myText, setMyText, getLatestMyText] = useExtendedState(
"default myText"
);
const myCellClickFunction = async (params) => {
setMyText(params.value);
const text = await getLatestMyText();
console.log("get latest state in callback", text);
};
...
}
Live Demo

React: Enzyme's it local scope does not work

I am using React. I used basic redux counter by using redux-toolkit. I am really new in testing. I am using Enzyme and Jest for the test. My redux counter intialState is 1. From my testing, inside it scope I first take the intialState then after simulate('click') increase button, I got result 2, which I expected. When I try to test my decrease button inside the it scope it takes the result from increase's it scope. If I put intialState 1 inside the decrease button's it scope, it gives me failed test because it expected 2.
This is my testing file
import React from 'react';
import { mount } from "enzyme"; // mount is fulldom renderning function with children
import Counter from 'components/counter';
import Root from "root/index"; // this is the root index which connect react component and redux
let wrapped;
beforeEach(() => {
wrapped = mount(
<Root>
<Counter />
</Root>
);
});
afterEach(() => {
wrapped.unmount(); // it cleans the mount after test.
});
describe(`This is counter component`, () => {
it(`This is show intial Value`, () => {
expect(wrapped.find(`h1`).text()).toEqual(`1`);
});
it(`after click it will increase the value`, () => {
expect(wrapped.find(`h1`).text()).toEqual(`1`);
wrapped.find(`button`).at(0).find(`[data-test="increment"]`).simulate(`click`);
expect(wrapped.find(`h1`).text()).toEqual(`2`);
});
it(`after click it will decrease the value`, () => {
expect(wrapped.find(`h1`).text()).toEqual(`1`); // test failed because it expect 2
wrapped.find(`button`).at(1).find(`[data-test="decrement"]`).simulate(`click`);
expect(wrapped.find(`h1`).text()).toEqual(`0`);
});
});
This is my counter component
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from 'store/reducer/counter/index';
import { IRootState } from 'store/combineReducer';
import styled from 'styled-components';
const Button = styled.button`
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
`;
const Text = styled.h1`
color: blue;
`;
export default () => {
const counter = useSelector((state: IRootState) => state.counter);
const dispatch = useDispatch();
return (
<div >
<Text>{counter}</Text>
<Button data-test="increment" onClick={() => dispatch(increment())}>
Increment counter
</Button>
<br></br>
<br></br>
<Button data-test="decrement" onClick={() => dispatch(decrement())}>
Decrement counter
</Button>
</div>
);
};
it block has a scope in a sense that a function has a scope, additionally other things like spies can be affected by currently running test. There are no problems with scopes here, the only thing that is affected by scopes is wrapper variable and it's defined in a scope that is common to all tests. Since it's reassigned in beforeEach, it cannot result in test cross-contamination.
The problem here is that there's global state because Redux naturally provides one.
Most commonly a custom store is set up for tests so initial value, etc. could be manipulated depending on testing needs:
let wrapped;
beforeEach(() => {
const store = ...
wrapped = mount(
<Provider store={store}><Counter /></Provider>
);
});
This is what the documentation recommends.
Alternatively, a store and all modules that directly depend on it needs to be re-imported per test, this should be done instead of top-level import of these modules:
let wrapped;
let Root;
beforeEach(() => {
jest.resetModules();
Root = require("root/index");
wrapped = mount(
<Root>
<Counter />
</Root>
);
});

React hook useRef not working with styled-components and typescript

I've some problem using the useRef hook with a styled component.
Linter alerts me that Object is possibly 'null' inside the didMount useEffect. Any idea about this?
This is not a duplicate for 2 reason:
The old answer refers to the ref used in a class component, that was the only way to use it before React hooks,
The innerRef props isn't supported anymore in the current version of styled components.
Here's a sample snippet of my component:
import React, { useRef, useEffect } from 'react';
import styled from 'styled-components';
const StyledInput = styled.input`
background: transparent;
`
const MyForm = () => {
const inputRef = useRef(null);
useEffect(() => {
if (inputRef && inputRef.current) {
inputRef.current.focus(); //Object is possibly 'null'
}
}, []);
return (
<StyledInput ref={inputRef}/>
);
}
I've finally found a solution:
const inputRef = useRef() as React.MutableRefObject<HTMLInputElement>;
It works for me:
import React, { useRef, useEffect } from 'react';
import styled from 'styled-components';
const StyledInput = styled.input`
background: transparent;
`
const MyForm = () => {
const inputRef = useRef() as React.MutableRefObject<HTMLInputElement>;
useEffect(() => {
if (inputRef && inputRef.current) {
inputRef.current.focus();
}
}, []);
return (
<StyledInput ref={inputRef}/>
);
}
As an alternative to the current accepted answer, you can also do:
const inputRef = useRef<HTMLInputElement>(null);
Pass your inputRef in the array argument of useEffect and let's see if that works, you aren't guaranteed to have a .current in your ref, so you should run the effect every time inputRef changes

Resources