Creating a Stepper using React and Redux - reactjs

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!! :)

Related

React-redux component rerenders every time when and unconnected state changes in the store . How do I prevent it?

I have a component callled FileHeader. Everytime I resize a column in it, it dispatches an action to changes a state. This triggers a rerender.
ListeHeader.js
const ListHeader = props => {
const dispatch = useDispatch()
let headerRef = useRef(new Array())
const { header_ } = useSelector((state) => state.ui, shallowEqual)
const onResize = ( e, {event, node, size}) => {
dispatch(changeHeaderWidth(node.id, size.width))
}
return (
<HeaderBody>
{header_
.map((header) => {
const getRef = (element) => headerRef.current.push(element)
return (
<ResizableBox
axis="x"
width={header.width}
height={20}
key={header.id}
handle={<DragHandle id={header.id} />}
onResize={onResize}
minConstraints={[50, 20]}
maxConstraints={[300, 20]}
>
<Header
key={header.id}
width={header.width}
handleDrag={handleDrag}
onClick={handleSort(header.id)}
>
<HeaderText ref={getRef}>{header.name}</HeaderText>
</Header>
</ResizableBox>
)
})}
</HeaderBody>
)
}
This is my reducer
export default (state = initial_state, actions) => {
switch (actions.type) {
case consts.CHANGE_HEADER_WIDTH : return {
...state,
headerWidth: state.headerWidth.map((item) =>
item.id === actions.payload.id ? { ...item, width: actions.payload.neWidth}
: item),
}
break;
default: return state;
}
}
I'm not calling headerWidth state in my component it causes a rerender when it changes
From the docs
When passing a callback using dispatch to a child component, you may
sometimes want to memoize it with useCallback. If the child component
is trying to optimize render behavior using React.memo() or similar,
this avoids unnecessary rendering of child components due to the
changed callback reference.
import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
const incrementCounter = useCallback(
() => dispatch({ type: 'increment-counter' }),
[dispatch]
)
return (
<div>
<span>{value}</span>
<MyIncrementButton onIncrement={incrementCounter} />
</div>
)
}
export const MyIncrementButton = React.memo(({ onIncrement }) => (
<button onClick={onIncrement}>Increment counter</button>
))

ContextProvider => Context APi : Children dynamically rendered

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>
);
};

How to get response after a function call in React JS?

Hi I am calling a function then trying to get response but it is saying undefined. I am passing that response from parent component to child component.
Component code is -
import React from 'react';
import {One, test, test1} from './Sample/SampleData';
let data:DataInterface = One;
const onChangeId = (Id:string) => {
switch (Id) {
case '1':
data = test
break;
case '2':
data = test1
break;
default:
data = One;
break;
}
console.log(data, " Data")
}
export const Test = () => {
return (
<div>
<ParentComponent
firstData={One}
onChangeId={onChangeId}
secondData={data}
/>
</div>
);
};
Parent Component code is -
import React from 'react';
export const ParentComponent = ({
firstData,
onChangeId,
secondData
}: NewInterface) => {
const [format, setFormat] = useState(secondData);
const onChange = (
type: string,
val:string
) => {
if (type === "welcome) {
onChangeId(val);
setTimeout(() => {
setFormat(secondData)
}, 2000)
console.log(secondData , "secondData")
}
};
return (
<React.Fragment>
<ChildComponent
onChange={onChange}
firstData={firstData}
newData={format}
/>
</React.Fragment>
);
}
When first tie component renders then I am getting secondData, but when I call onChangeId event I am getting secondData as undefined. So how can I resolve this issue ?
NOTE: We can't create seperate onChangeId event it must be inside onChange function as I am doing some other works too in ChildComponent.
Move useState hook outside of onChange method
export const ParentComponent = ({
firstData,
onChangeId,
secondData
}: NewInterface) => {
const [format, setFormat] = useState(secondData);
const onChange = (
type: string,
val:string
) => {
if (type === "welcome") {
onChangeId(val);
setTimeout(() => setFormat(secondData), 2000)
console.log(secondData , "secondData")
}
};

Get props from React container to functional component

This is how my container looks like:
class Shipping extends React.PureComponent {
constructor(props) {
super(props)
}
componentDidUpdate(prevProps) {
if (prevProps.match.params.shippingId !== this.props.match.params.shippingId) {
this.props.getShippingDetails(this.props.match.params.shippingId)
}
}
render = () => this.props.isLoading ? null : <ShippingView removeOrder={this.props.removeOrder} />
}
const mapStateToProps = ({ shippingDetails}) => ({
isLoading: shippingDetails.isLoading
})
const mapDispatchToProps = (dispatch) => ({
getShippingDetails: (id) => dispatch(shippingActions.getShippingDetails(id)),
removeOrder: (id) => dispatch(shippingActions.removeOrder(id))
})
export default () => Shared.accessPageWrapper([["acess:all"], ["admin:control", "navigation/shopping:view"]], (connect(mapStateToProps, mapDispatchToProps)(Shipping)), <Shared.AccessDeniedView />)
This is how my functional component looks like:
export const accessPageWrapper = (
permissionsAllowed = [],
component = null,
accessDeniedView,
accessDenied = true
) => {
const permissions = useSelector(state => state.user.permissions)
const permitted = permissionsAllowed.some((permission) => permission.every(el => permissions.includes(el)), false)
if (permitted) {
const Component = component
return <Component />
}
return accessDenied ? accessDeniedView : null
}
I'm not able to pass the props through the functional component as following:
const Component = component
return <Component {...props} />
Due to that issue, I'm getting the following error because my prop's prams are undefined.
Uncaught TypeError: Cannot read property 'params' of undefined
I have no idea how to fix this :/ Would you be so kind to help me?
Also, I don't want to change the above functional component to a class component.
Is there any way I can retrieve the props to the component? Thanks in advance!!
I think you are just missing the return of a component. Higher Order Components consume a component (and other possible parameters) and return a new, decorated component.
export const accessPageWrapper = (
permissionsAllowed = [],
component = null,
accessDeniedView,
accessDenied = true
) => (props) => { // <-- return a functional component
const permissions = useSelector((state) => state.user.permissions);
const permitted = permissionsAllowed.some(
(permission) => permission.every((el) => permissions.includes(el)),
false
);
if (component && permitted) { // <-- Ensure component exists!
const Component = component;
return <Component {...props} />; // <-- spread passed props to Component
}
return accessDenied ? accessDeniedView : null;
};
Probably need to update the export.
import { accessPageWrapper } from '.....';
...
export default accessPageWrapper(
[["acess:all"], ["admin:control", "navigation/shopping:view"]],
connect(mapStateToProps, mapDispatchToProps)(Shipping),
<Shared.AccessDeniedView />,
);

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.

Resources