ContextProvider => Context APi : Children dynamically rendered - reactjs

I have a function component named: Register. I use MUI (Material UI for ReactJS).
Inside it, I call 2 components: Step1 and Step2. Both use data collected through forms.
To simplify I don't post all the code.
<RegisterContextProvider>
{activeStep + 1 === 1 && (<Step1 handleNext={handleNext} />)}
{activeStep + 1 === 2 && (<Step2 handleNext={handleNext} handleBack={handleBack} />)}
</RegisterContextProvider>
When I click the "Next button" inside Step1, I go the next step of the MUI STEPPER component and render: Step2.
Step2 is rendered in the list of children of RegisterContextProvider, and Step1 disappears from the list, and then the data stored in the Step1 form is reinitialized.
Is there a way to wrap the components inside a higher level, like this:
<RegisterContextProvider>
<Register/>
</RegisterContextProvider>
Thank You for advice

You can create a Register component that will receive an array of objects with the data of the step components and the index of the current step.
Example:
import React, {useMemo} from 'react';
const outOfBoundsOfStepsData = {
Component: () => (<span>Index out of bounds of array</span>),
props: {}
};
const Register = ({steps, index}) => {
const {Component: Step, props} = useMemo(() => steps[index] || outOfBoundsOfStepsData, [steps, index]);
return <Step {...props} />;
};
const Step1 = () => {
// TODO
};
const Step2 = () => {
// TODO
};
const App = () => {
const steps = [
{
Component: Step1,
props: {
handleNext: () => console.log('TODO handleNext')
}
},
{
Component: Step2,
props: {
handleNext: () => console.log('TODO handleNext'),
handleBack: () => console.log('TODO handleBack')
}
}
];
const index = 0; // TODO
return (
<RegisterContextProvider>
<Register steps={steps} index={index}/>
</RegisterContextProvider>
);
};

Related

How can I prevent re render of react functional components if no props changed

Description:
I have a component imported in a loop in the Main component and whenever I update the state, the looped components are also re-rendered. So, how can I prevent them to render if no change is applied to the props of the component?
Eg:
const child1 = ({val}) => {
console.log("CHILD1 RENDERED")
return (
<>
{val}
</>
)
}
const main = () => {
const [number, setNumber] = useState(0);
const loopArr = [1, 2, 3, 4];
return (
<>
{
loopArr.map((item) => <child1 val={item}/>)
}
{number}
<button onClick={() => setNumber(number + 1)}>Increment</button>
</>
)
}
Use React.memo like so:
import { memo } from "react";
const Child2 = () => {
console.log("CHILD2 RENDERED");
return <div>Child2</div>;
};
export const MemoizedChild = memo(Child2);
Here is an example: Codesandbox.

Testing slider component in react

I want to test my slider component with react testing library. But I can't comprehend how to test it properly. I want to test changing slide when the user clicks the dot(StyledDotContainer). StyledDotContainer's background is gray but it is red when the active props is true. The component looks like this.
const Slider = ({
children,
autoPlayTime = 5000,
dots = true,
initialIndex= 0
}: SliderProps): React.ReactElement => {
const [activeIndex, setActiveIndex] = useState<number>(initialIndex)
const nextSlide = () => {
const newIndex = activeIndex >= length - 1 ? 0 : activeIndex + 1
setActiveIndex(newIndex)
}
useEffect(() => {
const timer = setTimeout(() => {
nextSlide()
}, autoPlayTime)
return () => clearTimeout(timer)
}, [activeIndex])
const length = useMemo(() => {
return React.Children.count(children)
}, [])
const setSlide = useCallback((index: number) => {
setActiveIndex(index)
}, [])
const value = useMemo(() => ({ activeIndex, setSlide }), [activeIndex])
return (
<SliderContext.Provider value={value}>
<StyledContainer>
{children}
{dots && (
<StyledDotsContainer data-testid="dots">
{[...Array(length)].map((_, index) => {
return (
<StyledDotContainer
data-testid={`dot-${index}`}
key={index}
onClick={() => setSlide(index)}
isActive={index === activeIndex}
/>
)
})}
</StyledDotsContainer>
)}
</StyledContainer>
</SliderContext.Provider>
)
}
Appreciate any suggestion.
It's a bad practice to test the styles of a Component. Instead you just want to test that the function you are trying to test is properly changing the props in your component. Styles should be inspected visually.
import screen from '#testing-library/dom'
import {render, screen} from '#testing-library/react'
import userEvent from '#testing-library/user-event'
describe("Slider", () => {
it('Should be red when slider is active', () => {
render(Slider)
const firstDot = screen.getByTestId('dots-0')
act(() => {
userEvent.click(firstDot)
})
waitFor(() => {
expect(screen.getByTestId('dots-0').props.isActive).toBeTruthy()
})
})
})

How to test an if sentence inside useEffect?

I'm trying to test an if sentence inside a useEffect which main dependency, riskSelection, comes from a useSelector from react-redux library. Jest coverage indicates line 20-21 is missing.
App.tsx
function App(): JSX.Element {
const riskSelection = useAppSelector(selectRiskSelection);
const {save} = useActions({...riskSelectorActions});
const risks: Risk = risks_levels;
const [data, setData] = useState<Data[]>();
const width = 450,
height = 450,
margin = 40;
const radius = Math.min(width, height) / 2 - margin;
useEffect(() => {
if (typeof riskSelection !== 'undefined') { // line 20
setData(Object.values(risks[riskSelection])); // line 21
}
}, [riskSelection, risks]);
return (
<div>
<ul className={style.riskSelectorUl}>
{new Array(10).fill(0).map((v, i: number) => (
<li
className={Number(riskSelection) === i + 1 ? style.selected : ''}
onClick={() => save({riskSelection: `${i + 1}`})}
key={i}
>
{i + 1}
</li>
))}
</ul>
{data && (
<PieSVG
data={data}
width={width}
height={height}
innerRadius={100}
outerRadius={radius}
/>
)}
</div>
);
}
export default App;
My test should verify if PieSVG has been rendered or not. In order to do so I have to simulate a change in riskSelection. This is where I need help.
App.test.tsx
jest.mock('../utils/redux/hooks');
import * as reduxHooks from '../utils/redux/hooks';
describe('App', () => {
let wrapper: any;
beforeEach(() => {
jest
.spyOn(reduxHooks, 'useAppSelector')
.mockImplementation((f) => f({riskSelector: {riskSelection: undefined}}));
wrapper = shallow(<Dummy />);
});
afterEach(() => {
jest.clearAllMocks();
});
it('shows pie chart', () => {
reduxHooks.useAppSelector(() => '1');
expect(wrapper.find(PieSVG)).toHaveLength(1);
});
});
I use mockImplementation to try to change riskSelector but that just initializes my redux state.
The user trigger this change of redux state when it click on a <li /> which uses save action.
Maybe I should not make my UI changes so dependent on a useEffect?
hook.ts
import {TypedUseSelectorHook, useSelector} from 'react-redux';
import type {RootState} from './store';
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

React hooks useEffect calls mutiple times when redux store other data changed

my code like this:
Info component:
import {
getAttachData,
} from '#src/actions/creators/account'
const Info: React.FC = () => {
const info = useSelector<any, Account>(state => state.getIn(['account', 'info']).toJS())
const list = useSelector<any, Data[]>(state => state.getIn(['account', 'list']).toJS())
const attach = useSelector<any, AttachData[]>(state => state.getIn(['account', 'attach']).toJS())
...
const handleChange = ({ select }) => {
dispatch(getAttachData({v: select}))
}
const Template = (params) => {
return (
<div>
<BaseSelect onChange={(val) => handleChange(val)} list={list} />}
</div>
)
}
return (
...
<Template data={info} />
{attach.map((child, cidx) => (<Template data={child} />))}
)
}
export default Info
BaseSelect component:
const BaseSelect: React.FC<Props> = props => {
const [selectId, setSelectId] = useState('')
const { list } = props
useEffect(() => {
if (!isEmpty(list)) {
...
}
console.log('init')
}, [])
const handleChange = (value) => {
setSelectId(value)
props.onChange({
select: value,
})
}
return (
<Select
data={list}
value={selectId}
onChange={handleChange}
/>
)
}
export default BaseSelect
when excute handleChange event in BaseSelect component, the props.onChange function will call handleChange event in info component, and dispatch http request getAttachData which will change attach data in redux store, but useEffect in BaseSelect component will also excute and in console will print 'init' two times.
console:
It's because your Template component re-creates every time when redux store is changing.
Just move Template component outside the Info component.

Creating a Stepper using React and Redux

I am trying to create a Stepper using react and redux. The concept I am using for it is -
Store the active step in redux store.
I have a list of components and each component is associated with an index.
In parent component, I render the component whose index is equal to active step.
Here's the code for parent component -
I have component step map -
const COMPONENT_STEP_MAP = {
1: (props) => (
<Component1
{...props}
render = {(props) => <Buttons {...props}/>}
></Component1>
),
2: (props) => (
<Component2
{...props}
render = {(props) => <Buttons {...props}/>}
></Component2>
),}
Here's How my Redux Store looks like -
const initialState = {
activeTab: 1,
basicDetails: {
activeStep: 1,
name: '',
}
Here's the render function of the parent component -
export class ParentComponent extends React.component {
handleNext = () => {
if(this.props.activeStep == 1 &&
this.props.isBusinessOwner==false){ // isBusinessOwner is not showing correct value.
this.props.setActiveStep(this.props.activeStep + 2)
}
else this.props.setActiveStep(this.props.activeStep + 1);
};
handlePrevious = () => {
if(this.props.activeStep == 1 &&
this.props.isBusinessOwner==false){
this.props.setActiveStep(this.props.activeStep - 2);
} else
this.props.setActiveStep(this.props.activeStep - 1)
};
render() {
return ({Object.entries(COMPONENT_STEP_MAP).map((comp) => {
return comp[0] == this.props.activeStep
? comp[1]({handlePrevious, handleNext}):null}}) } }
I am using react-redux to connect it to the store -
const mapStateToProps = (state) => ({activeStep: state.ownerDetails.activeStep, isBusinessOwner: state.ownerDetails.isBusinessOwner});
const mapDispatchToProps = (dispatch) => ({
setActiveStep: (step) => dispatch({type: 'ownerDetails', payload:{activeStep: step}})
})
export default connect(mapStateToProps, mapDispatchToProps)(OwnerDetails);
Now I have following child component
import React from 'react';
import {useSelector, useDispatch} from 'react-redux';
export function IsOwner(props) {
const isOwner = useSelector(state => state.ownerDetails.isBusinessOwner);
const setIsBusinessOwner = useDispatch();
const handleChange = (value) => {
// console.log('value', value);
setIsBusinessOwner({type: 'ownerDetails', payload: {isBusinessOwner: value}})
props.handleNext();
};
return (
<div>
<h4>Are You Business Owner</h4>
<button onClick={handleChange.bind(null,true)}>Yes</button>
<button onClick={handleChange.bind(null,false)}>No</button>
</div>
)
}
I have doubt in following 2 lines -
setIsBusinessOwner({type: 'ownerDetails', payload: {isBusinessOwner: value}})
props.handleNext();
setIsBusinessOwner updates the store and will force the component to re-render.
However, I am immediately calling props.handleNext() after it , and component will be gone from the DOM.
So, When I access isBusinessOwner from store in parent component. It is reflecting the previous value not the updated value.
Any suggestions on how to fix this issue ?
Any help will be greatly appreciated.
Thanks in advance!! :)

Resources