Problem with React Hook "useCallback" and Pure components - performance

Before React Hooks, I had a code like this:
interface State {
f1: number;
f2: number;
}
class OldClassParent extends React.Component<{}, State> {
constructor(props: {}) {
super(props);
this.state = {
f1: 0,
f2: 0
};
}
public render() {
const { f1, f2 } = this.state;
return (
<>
{/* Each pure component re-render only if its value has changed (OK) */}
<MyPureCmp value={f1} name="f1" onChange={this.onChange} />
<MyPureCmp value={f2} name="f2" onChange={this.onChange} />
</>
);
}
private onChange = (name: string, newValue: number) => {
// #ts-ignore
this.setState({
[name]: newValue
});
}
}
Now, I try to have the same behavior with React-hooks. I made the following:
const NewHookParent: React.FC = () => {
const [state, setState] = useState({
f1: 0,
f2: 0
});
// useCallback does not help me here
const onChange = useCallback((name: string, newValue: number) => {
setState({...state, [name]: newValue});
}, [state, setState]);
const { f1, f2 } = state;
return (
<>
{/* Each pure component re-render when any value change
as the onChange handler is never the same (NOT OK) */}
<MyPureCmp value={f1} name="f1" onChange={onChange} />
<MyPureCmp value={f2} name="f2" onChange={onChange} />
</>
);
}
The problem is that I have lost the re-rendering optimization I had before.
In this example, for simplicity, I used only two fields, but actually I may have any number of fields (and it can be dynamic, not known at compile time).
What should I do ?

The setState function produced by useState, accepts an updater function. The updater is called with the current state. Using this allows replaces passing the state to useCallback, and since the setState itself won't change, useCallback will return the same function all the time.
const onChange = useCallback((name: string, newValue: number) => {
setState(state => ({...state, [name]: newValue}));
}, [setState]);

Related

Why need setState func Inside useEffect func on this code

import { useRef, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import styled from 'styled-components'
/** HeaderModal custom hooks */
const useHeaderModal = () => {
const [modalOpened, setModalOpened] = useState(false)
const openModal = () => {
setModalOpened(true)
}
const closeModal = () => {
setModalOpened(false)
}
interface IProps {
children: React.ReactNode
}
const ModalPortal: React.FC<IProps> = ({ children }) => {
const ref = useRef<Element | null>()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
if (document) {
const dom = document.querySelector('#root-modal')
ref.current = dom
}
}, [])
if (ref.current && mounted && modalOpened) {
return createPortal(
<Container>
<div
className="modal-background"
role="presentation"
onClick={closeModal}
></div>
{children}
</Container>,
ref.current,
)
}
return null
}
return {
openModal,
closeModal,
ModalPortal,
}
}
export default useHeaderModal
This code is custom hook for opening modal.
And I don't understand 'Why need setState func Inside useEffect func on this code?'.
When I eliminate
setMounted(true), ref was not created.
And then in case clicking modal open button
ref was not much created.
When the button is pressed only when the setMounted function
is put in useEffect
The ref is created and then the modal is opened.
Could you please tell me why?

What's is wrong in useTypeWriter hook algorithm

I'm trying to create a type writer hook in React, but I can't identify in the algorithm what's not working. I don't know if useState is the most viable to hold all variables. The characters & and . will be rendered with styling, so I created a checkLetter function to check them and return a span styled in Tailwind. The state has a variable named writing to hold the each letter as string and a stylized span (ampersando and point) as ReactElement. The algorithm is in an infinite loop and is not adding letter by letter. I can't understand why since I put the conditionals on top of isWriting in useEffect so that only one scope is executed. When isWriting is true then the word is being written, when is false it is being deleted. That's how it should be at least. The algorithm receives delay for the beginning of each sentence, between each word and for the end of the written word until it starts deleting letter by letter. The stringsIdx serves to pull the sentence from the array of strings received in the component, and the letterIdx to pull each letter from that string, at the end of the type writer loop it goes to the next sentence until it returns to the first. The hook returns the array to be rendered in JSX.
import React, { useEffect, useState } from 'react';
import { nanoid } from '#reduxjs/toolkit';
export const useTypeWriter = (
strings: string[],
writingDelay: number,
initialDelay: number,
finalDelay: number
) => {
const [state, setState] = useState<{
writing: Array<React.ReactElement | string>;
isWriting: boolean;
letterIdx: number;
stringsIdx: number;
}>({
writing: [],
isWriting: true,
letterIdx: 0,
stringsIdx: 0,
});
const string = strings[state.stringsIdx];
const checkLetter = (
letter: string,
arr: Array<React.ReactElement | string>
) => {
let ampersand: React.ReactElement;
let point: React.ReactElement;
if (letter === '&') {
ampersand = (
<span key={nanoid()} className='font-bold text-primary'>
&
</span>
);
return [...arr, ampersand];
}
if (letter === '.') {
point = (
<span
key={nanoid()}
className='font-bold text-primary font-anton animate-pulse'
>
.
</span>
);
return [...arr, point];
}
return [...arr, letter];
};
useEffect(() => {
let timer;
if (state.isWriting) {
if (state.writing.length === string.length) {
timer = setTimeout(
() =>
setState((state) => ({
...state,
isWriting: false,
})),
initialDelay
);
} else {
setTimeout(
() =>
setState((state) => ({
...state,
writing: checkLetter(string[state.letterIdx], state.writing),
letterIdx: state.letterIdx + 1,
})),
writingDelay
);
}
}
if (!state.isWriting) {
if (state.writing.length === string.length) {
timer = setTimeout(
() =>
setState((state) => ({
...state,
isWriting: true,
})),
finalDelay
);
} else {
setTimeout(
() =>
setState((state) => ({
...state,
writing: checkLetter(string[state.letterIdx], state.writing),
letterIdx: state.letterIdx - 1,
})),
writingDelay
);
}
}
console.log(state, string);
return () => {
clearTimeout(timer);
};
}, [string, state, setState, writingDelay, initialDelay, finalDelay]);
return state.writing;
};

Uncaught TypeError: Cannot read properties of undefined (reading 'target'),

My problem is that how do i access 'handleInputChange', because i cant write 'handleInputChange' function outside the useEffect hook since it is performing a sideeffect. I would love it if someone can help me out with this situation.
1. const [values, setValue] = useState({});
const dispatch = useDispatch();
let handleInputChange
useEffect(()=>{
handleInputChange = (e) =>{
setValue(
{
values: { ...values, [e.target.name]: e.target.value }
},
() => {
dispatch({
type: "SET_FORMVALUES",
payload: values
})
}
)}
handleInputChange();
},[dispatch])
<TextField id="outlined-basic" label="First Name" variant="outlined"
name="firstName"
className='app__input'
placeholder='First Name'
type="text"
value={values['firstName']}
onChange = {handleInputChange} />
//Reducer.js
const initialState = {
formValues: {},
message: ""
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case "SET_FORMVALUES":
console.log("--- Changing form data ---");
return {
...state,
formValues: action.payload
};
case "SUBMIT_FORM":
return {
...state,
message: "Form submitted!!"
};
default:
return state;
}
};
First, you don't need the core React useState hook, because you are using React Redux. This is actually creating a second state local to your component. Instead, use the centralized Redux store you've configured and React-Redux hooks. As long as you have wrapped your app in a context provider and passed your store to it, you can use useDispatch to update state and useSelector to retrieve state and subscribe to changes.
Second, you don't need useEffect, as you are just updating state.
Here's an example:
import { useDispatch, useSelector } from 'react-redux';
export default function App() {
const formValues = useSelector((state) => state.formValues);
const dispatch = useDispatch();
const handleInputChange = (name, value) => {
dispatch(
{
type: "SET_FORMVALUES",
payload: {
...formValues,
[name]: value
}
}
);
}
return (
<div className="App">
<input type="text" name="FirstName" onChange={ (e) => handleInputChange(e.target.name, e.target.value)} />
<span>{formValues["FirstName"]}</ span>
<input type="text" name="LastName" onChange={ (e) => handleInputChange(e.target.name, e.target.value)} />
<span>{formValues["LastName"]}</ span>
</div>
);
}
Much of this is probably not directly related to the error in the question title, but simplifying your code should help you debug more easily. That error may have been simply because you didn't explicitly pass the event in your onChange handler. I.e. onChange = {handleInputChange} vs. onChange = {(e) => handleInputChange(e)}

How to load AJAX and update state in componentDidUpdate?

I have a component that renders a list of credit cards. It receives a contract as a prop. A contract id can have multiple related credit cards. This list is fetched from the API via AJAX. The component can be visible or hidden on screen.
I fetch the list of credit cards via AJAX in state cards which is a tuple (key/value): [number, ICreditCardRecord[]]. That way I can keep track of which contract id the list I have in state belongs to.
So in componentDidUpdate I check if the component is not hidden and if the selected contract matches the one on which I saved the list. If not, I fetch the list again for the new contract.
Problem is, it triggers an endless loop. I read that it's not possible to setState inside componentDidUpdate as it triggers a re-render which triggers the function again which ... (endless loop).
So how can I do this? BTW I also use state to manage whether to show loading overlay or not (isLoading: boolean). I update this state prop when ever I start/stop loading.
import React, { Component } from 'react'
import { IApiVehicleContract, ICreditCardRecord } from '#omnicar/sam-types'
import * as api from 'api/api'
import { WithStyles, withStyles } from '#material-ui/core'
import CreditCard from 'components/customer/Contract/Details/CreditCard'
import NewCreditCard from 'components/customer/Contract/Details/NewCreditCard'
import AddCardDialog from 'components/customer/Contract/Details/AddCardDialog'
import { AppContext } from 'store/appContext'
import LoadingOverlay from 'components/LoadingOverlay'
import styles from './styles'
import alertify from 'utils/alertify'
import { t } from '#omnicar/sam-translate'
import { loadScript } from 'utils/script'
interface IProps extends WithStyles<typeof styles> {
contract: IApiVehicleContract | undefined
hidden: boolean
}
interface IState {
cards: [number, ICreditCardRecord[]]
showAddCreditCardDialog: boolean
isStripeLoaded: boolean
isLoading: boolean
}
class CustomerContractDetailsTabsCreditCards extends Component<IProps, IState> {
public state = {
cards: [0, []] as [number, ICreditCardRecord[]],
showAddCreditCardDialog: false,
isStripeLoaded: false,
isLoading: false,
}
public componentDidUpdate(prevProps: IProps, prevState: IState) {
const { cards } = this.state
const { contract, hidden } = this.props
// selected contract changed
const changed = contract && contract.serviceContractId !== cards[0]
// visible and changed
if (!hidden && changed) {
this.getCreditCards()
console.log('load')
}
}
public async componentDidMount() {
// load stripe
const isStripeLoaded = await loadScript('https://js.stripe.com/v3/', 'stripe-payment')
this.setState({ isStripeLoaded: !!isStripeLoaded })
}
public render() {
const { classes, contract, hidden } = this.props
const { cards, showAddCreditCardDialog, isLoading } = this.state
return (
<div className={`CustomerContractDetailsTabsCreditCards ${classes.root} ${hidden && classes.hidden}`}>
<React.Fragment>
<ul className={classes.cards}>
{cards[1].map(card => (
<CreditCard
className={classes.card}
key={card.cardId}
card={card}
onDeleteCard={this.handleDeleteCard}
onMakeCardDefault={this.handleMakeDefaultCard}
/>
))}
<NewCreditCard className={classes.card} onToggleAddCreditCardDialog={this.toggleAddCreditCardDialog} />
</ul>
<AppContext.Consumer>
{({ stripePublicKey }) => {
return (
<AddCardDialog
open={showAddCreditCardDialog}
onClose={this.toggleAddCreditCardDialog}
contract={contract!}
onAddCard={this.handleAddCard}
stripePublicKey={stripePublicKey}
isLoading={isLoading}
/>
)
}}
</AppContext.Consumer>
</React.Fragment>
<LoadingOverlay open={isLoading} />
</div>
)
}
private getCreditCards = async () => {
const { contract } = this.props
if (contract) {
// this.setState({ isLoading: true })
const res = await api.getContractCreditCards(contract.prettyIdentifier)
debugger
if (res) {
if (res.errorData) {
// this.setState({ isLoading: false })
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
console.error(res.errorData.message)
return
}
// sort list: put active first
const creditCards: ICreditCardRecord[] = res.data!.sort((a, b) => +b.isDefault - +a.isDefault)
const cards: [number, ICreditCardRecord[]] = [contract.serviceContractId, creditCards]
debugger
this.setState({ cards, isLoading: false })
}
}
}
private handleDeleteCard = async (cardId: string) => {
const { contract } = this.props
// show spinner
this.setState({ isLoading: true })
const req = await api.deleteCreditCard(contract!.prettyIdentifier, cardId)
if (req && (req.errorData || req.networkError)) {
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
// hide spinner
this.setState({ isLoading: false })
return console.error(req.errorData || req.networkError)
}
// remove card from list
const creditCards = this.state.cards[1].filter(card => card.cardId !== cardId)
const cards: [number, ICreditCardRecord[]] = [this.state.cards[0], creditCards]
// update cards list + hide spinner
this.setState({ isLoading: false, cards })
// notify user
alertify.success(t('Credit card has been deleted'))
}
private handleMakeDefaultCard = async (cardId: string) => {
const { contract } = this.props
// show spinner
this.setState({ isLoading: true })
const req = await api.makeCreditCardDefault(contract!.prettyIdentifier, cardId)
if (req && (req.errorData || req.networkError)) {
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
// hide spinner
this.setState({ isLoading: false })
return console.error(req.errorData || req.networkError)
}
// show new card as default
const creditCards = this.state.cards[1].map(card => {
const res = { ...card }
if (card.cardId !== cardId) {
res.isDefault = false
} else {
res.isDefault = true
}
return res
})
const cards: [number, ICreditCardRecord[]] = [this.state.cards[0], creditCards]
// update cards list + hide spinner
this.setState({ isLoading: false, cards })
// notify user
alertify.success(t('Credit card is now default'))
}
private toggleAddCreditCardDialog = () => {
this.setState({ showAddCreditCardDialog: !this.state.showAddCreditCardDialog })
}
private appendNewCardToList = (data: any) => {
const {cards: cardsList} = this.state
let creditCards = cardsList[1].map(card => {
return { ...card, isDefault: false }
})
creditCards = [data, ...creditCards]
const cards: [number, ICreditCardRecord[]] = [cardsList[0], creditCards]
this.setState({ cards })
// notify user
alertify.success(t('Credit card has been added'))
}
private handleAddCard = (stripe: stripe.Stripe) => {
// show spinner
this.setState({ isLoading: true })
const stripeScript = stripe as any
stripeScript.createToken().then(async (result: any) => {
if (result.error) {
// remove spinner
this.setState({ isLoading: false })
// notify user
alertify.warning(t('An error occurred... Please contact OmniCar if the problem persists. '))
return console.error(result.error)
}
// add credit card
const prettyId = this.props.contract ? this.props.contract.prettyIdentifier : ''
const req = await api.addCreditCard({ cardToken: result.token.id, isDefault: true }, prettyId)
if (req.errorData) {
alertify.warning(t('An error occurred while trying to add credit card'))
return console.error(req.errorData)
}
// remove spinner and close dialog
this.setState({ isLoading: false }, () => {
this.toggleAddCreditCardDialog()
this.appendNewCardToList(req.data)
})
})
}
}
export default withStyles(styles)(CustomerContractDetailsTabsCreditCards)

ReactJS pass props to child via redux ajax

I have a reactjs component with redux which passes asynchronously props to child component.
In child component I try to catch the data in componentDidMount but somehow does not work either, however the child component is getting rendered.
This is my parent component
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as slidesActions from '../../actions/slidesActions';
import Slider from '../Partials/Slider'
import _ from 'underscore';
class HomePage extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.actions.getSlides()
}
componentWillMount() {
const {slides} = this.props;
}
render() {
const {slides} = this.props;
return (
<div className="homePage">
<Slider columns={1} slides={slides} />
</div>
);
}
}
function mapStateToProps(state) {
return {
slides: state.slides
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(slidesActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
here comes my child component where I try to get passed slides props but is empty
import React from 'react';
import _ from 'underscore';
import Hammer from 'hammerjs';
class Slider extends React.Component {
constructor(props) {
super(props)
this.updatePosition = this.updatePosition.bind(this);
this.next = this.next.bind(this);
this.prev = this.prev.bind(this);
this.state = {
images: [],
slidesLength: null,
currentPosition: 0,
slideTransform: 0,
interval: null
};
}
next() {
const currentPosition = this.updatePosition(this.state.currentPosition - 10);
this.setState({ currentPosition });
}
prev() {
//TODO: work on logic
if( this.state.currentPosition !== 0) {
const currentPosition = this.updatePosition(this.state.currentPosition + 10);
this.setState({currentPosition});
}
}
componentDidMount() {
//here I try set a state variable on slides
let {slides} = this.props
let slidesLength = slides.length
this.setState({slidesLength})
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
}
componentWillUnmount() {
this.hammer.off('swipeleft', this.next)
this.hammer.off('swiperight', this.prev)
}
updatePosition(nextPosition) {
const { visibleItems, currentPosition } = this.state;
return nextPosition;
}
render() {
let {slides, columns} = this.props
let {currentPosition} = this.state
let sliderNavigation = null
//TODO: this should go to slides actions
let slider = _.map(slides, function (slide) {
let Background = slide.featured_image_url.full;
if(slide.status === 'publish')
return <div className="slide" id={slide.id} key={slide.id}><div className="Img" style={{ backgroundImage: `url(${Background})` }} data-src={slide.featured_image_url.full}></div></div>
});
if(slides.length > 1 ) {
sliderNavigation = <ul className="slider__navigation">
<li data-slide="prev" className="" onClick={this.prev}>previous</li>
<li data-slide="next" className="" onClick={this.next}>next</li>
</ul>
}
return <div ref={
(el) => this._slider = el
} className="slider-attached"
data-navigation="true"
data-columns={columns}
data-dimensions="auto"
data-slides={slides.length}>
<div className="slides" style={{ transform: `translate(${currentPosition}%, 0px)`, left : 0 }}> {slider} </div>
{sliderNavigation}
</div>
}
}
export default Slider;
and here I have my actions for slider
import * as types from './actionTypes';
import axios from 'axios';
import _ from 'underscore';
//TODO: this should be accessed from DataService
if (process.env.NODE_ENV === 'development') {
var slidesEndPoint = 'http://dev.server/wp-json/wp/v2/slides';
} else {
var slidesEndPoint = 'http://prod.server/wp-json/wp/v2/slides';
}
export function getSlides () {
return dispatch => {
dispatch(setLoadingState()); // Show a loading spinner
axios.get(slidesEndPoint)
.then(function (response) {
dispatch(setSlides(response.data))
dispatch(doneFetchingData(response.data))
})
/*.error((response) => {
dispatch(showError(response.data))
})*/
}
}
function setSlides(data) {
return {
type: types.SLIDES_SUCCESS,
slides: data
}
}
function setLoadingState() {
return {
type: types.SHOW_SPINNER,
loaded: false
}
}
function doneFetchingData(data) {
return {
type: types.HIDE_SPINNER,
loaded: true,
slides: data
}
}
function showError() {
return {
type: types.SHOW_ERROR,
loaded: false,
error: 'error'
}
}
Reason is, componentDidMount will get called only once, just after the initial rendering, since you are fetching the data asynchronously so before you get the data Slider component will get rendered.
So You need to use componentwillreceiveprops lifecycle method.
componentDidMount:
componentDidMount() is invoked immediately after a component is
mounted. Initialization that requires DOM nodes should go here. If you
need to load data from a remote endpoint, this is a good place to
instantiate the network request. Setting state in this method will
trigger a re-rendering.
componentWillReceiveProps:
componentWillReceiveProps() is invoked before a mounted component
receives new props. If you need to update the state in response to
prop changes (for example, to reset it), you may compare this.props
and nextProps and perform state transitions using this.setState() in
this method.
Write it like this:
componentWillReceiveProps(nextProps){
if(nextProps.slides){
let {slides} = nextProps.props
let slidesLength = slides.length;
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
this.setState({slidesLength})
}
}
As far as I understand, you are doing an axios call to fetch the data and then set it in the reducer which you are returning later. Also initially reducer data is empty . Now since componentDidMount is called only once, and initially no data may have been there you are not seeing any values. Use a componentWillReceiveProps function
componentWillReceiveProps(nextProps) {
//here I try set a state variable on slides
let {slides} = nextProps
let slidesLength = slides.length
this.setState({slidesLength})
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
}

Resources