I am creating a multistep form in React which uses a switch case to render a component based on its ID:
App.js
function App() {
const steps = [
{ id: 'location' },
{ id: 'questions' },
{ id: 'appointment' },
{ id: 'inputData' },
{ id: 'summary' },
];
return (
<div className="container">
<ApptHeader steps={steps} />
<Step steps={steps} />
</div>
);
}
Steps.js
const Step = ({ steps }) => {
const { step, navigation } = useStep({ initialStep: 0, steps });
const { id } = step;
const props = {
navigation,
};
console.log('StepSummary', steps);
switch (id) {
case 'location':
return <StepLocation {...props} steps={steps} />;
case 'questions':
return <StepQuestions {...props} />;
case 'appointment':
return <StepDate {...props} />;
case 'inputData':
return <StepUserInputData {...props} />;
case 'summary':
return <StepSummary {...props} />;
default:
return null;
}
};
In my <ApptHeader /> component in App.js, I want to change the Title and subtitle of the string in the header based on the component rendered in the switch case.
const SectionTitle = ({ step }) => {
console.log('step', step);
return (
<div className="sectionWrapper">
<div className="titleWrapper">
<div className="title">Title</div>
<div className="nextStep">
SubTitle
</div>
</div>
<ProgressBar styles={{ height: 50 }} />
</div>
);
};
export default SectionTitle;
How can I accomplish this? I feel like I might be writing redundant code if I have to make a switch case again for each title/subtitle. Thanks in advance.
You can use a pattern like this
const steps = {
location: { id: 'location', title: 'Location', component: StepLocation },
questions: { id: 'questions', title: 'Questions', component: StepQuestions },
appointment: { id: 'appointment', title: 'Appointment', component: StepDate },
inputData: { id : 'inputData', title: 'InputData', component: StepUserInputData },
summary: { id: 'summary', title: 'Summary', component: StepSummary },
};
Then while using it inside your Steps.js will become
const Step = ({ steps }) => {
const { step, navigation } = useStep({ initialStep: 0, steps });
const { id } = step;
const props = { navigation };
const Component = steps[id].component;
return <Component {...props} steps={steps} />;
};
Your SectionTitle.js will become like this
const SectionTitle = ({ step }) => {
console.log('step', step);
return (
<div className="sectionWrapper">
<div className="titleWrapper">
<div className="title">Title</div>
<div className="nextStep">{step.title}</div>
</div>
<ProgressBar styles={{ height: 50 }} />
</div>
);
};
export default SectionTitle;
This way you can avoid code redundancy.
Be Sure to update your other parts of code like useStep to accept and Object instead of and Array
Related
I have a simple component that renders a menu with items. What I am trying to do is have a value called isLoggedIn that accesses the value of the Italian Food item, change it's value to true and later hide the Italian Food item. Currenly my code works, the Italian Restaurant item gets hidden, but is there a better way to access the available property, change it based on a condition and to hide the element? Here is my code:
import React, { useState, useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import {
Drawer,
DrawerContent,
DrawerItem,
} from '#progress/kendo-react-layout';
import { Button } from '#progress/kendo-react-buttons';
const CustomItem = (props) => {
const { visible, ...others } = props;
const [isLoggedIn, setIsLoggedIn] = React.useState(
props.available ? false : true
);
const arrowDir = props['data-expanded']
? 'k-i-arrow-chevron-down'
: 'k-i-arrow-chevron-right';
React.useEffect(() => {
setIsLoggedIn(props.available);
}, [props.available]);
return (
<React.Fragment>
{isLoggedIn === false ? null : (
<DrawerItem {...others}>
<span className={'k-icon ' + props.icon} />
<span className={'k-item-text'}>{props.text}</span>
{props['data-expanded'] !== undefined && (
<span
className={'k-icon ' + arrowDir}
style={{
position: 'absolute',
right: 10,
}}
/>
)}
</DrawerItem>
)}
</React.Fragment>
);
};
const DrawerContainer = (props) => {
const [drawerExpanded, setDrawerExpanded] = React.useState(true);
const [items, setItems] = React.useState([
{
text: 'Education',
icon: 'k-i-pencil',
id: 1,
selected: true,
route: '/',
},
{
separator: true,
},
{
text: 'Food',
icon: 'k-i-heart',
id: 2,
['data-expanded']: true,
route: '/food',
},
{
text: 'Japanese Food',
icon: 'k-i-minus',
id: 4,
parentId: 2,
route: '/food/japanese',
},
{
text: 'Italian Food',
icon: 'k-i-minus',
id: 5,
parentId: 2,
route: '/food/italian',
available: false,
},
{
separator: true,
},
{
text: 'Travel',
icon: 'k-i-globe-outline',
['data-expanded']: true,
id: 3,
route: '/travel',
},
{
text: 'Europe',
icon: 'k-i-minus',
id: 6,
parentId: 3,
route: '/travel/europe',
},
{
text: 'North America',
icon: 'k-i-minus',
id: 7,
parentId: 3,
route: '/travel/america',
},
]);
const handleClick = () => {
setDrawerExpanded(!drawerExpanded);
};
const onSelect = (ev) => {
const currentItem = ev.itemTarget.props;
const isParent = currentItem['data-expanded'] !== undefined;
const nextExpanded = !currentItem['data-expanded'];
const newData = items.map((item) => {
const {
selected,
['data-expanded']: currentExpanded,
id,
...others
} = item;
const isCurrentItem = currentItem.id === id;
return {
selected: isCurrentItem,
['data-expanded']:
isCurrentItem && isParent ? nextExpanded : currentExpanded,
id,
...others,
};
});
props.history.push(ev.itemTarget.props.route);
setItems(newData);
};
const data = items.map((item) => {
const { parentId, ...others } = item;
if (parentId !== undefined) {
const parent = items.find((parent) => parent.id === parentId);
return { ...others, visible: parent['data-expanded'] };
}
return item;
});
return (
<div>
<div className="custom-toolbar">
<Button icon="menu" look="flat" onClick={handleClick} />
<span className="title">Categories</span>
</div>
<Drawer
expanded={drawerExpanded}
mode="push"
width={180}
items={data}
item={CustomItem}
onSelect={onSelect}
>
<DrawerContent>{props.children}</DrawerContent>
</Drawer>
</div>
);
};
export default withRouter(DrawerContainer);
If I understood your request properly you want to calculate the isLoggedIn property based on props.available, right? If this is correct then you may just use the useMemo hook in the following way:
const CustomItem = (props) => {
const { visible, ...others } = props;
const isLoggedIn = React.useMemo(() => {
return !props.available
});
const arrowDir = props['data-expanded']
? 'k-i-arrow-chevron-down'
: 'k-i-arrow-chevron-right';
return (
<React.Fragment>
{isLoggedIn === false ? null : (
<DrawerItem {...others}>
<span className={'k-icon ' + props.icon} />
<span className={'k-item-text'}>{props.text}</span>
{props['data-expanded'] !== undefined && (
<span
className={'k-icon ' + arrowDir}
style={{
position: 'absolute',
right: 10,
}}
/>
)}
</DrawerItem>
)}
</React.Fragment>
);
};
Here the doc of the hook if you want to go deeper.
I am coding a recursive component.
If I pass props like this: <RecursiveComponent itemProps={data} />, it won't work. I have to pass like this: <RecursiveComponent {...data} />. Why is that?
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
import * as React from 'react'
const data = {
name: 'Level 1',
children: [
{
name: 'Level 2',
children: [
{
name: 'Level 3'
}
]
},
{
name: 'Level 2.2',
children: [
{
name: 'Level 3.2'
}
]
}
]
}
const RecursiveComponent = (itemsProp) => {
const hasData = itemsProp.children && itemsProp.children.length;
return (
<>
{itemsProp.name}
{hasData && itemsProp.children.map((child) => {
console.log(child);
return <RecursiveComponent {...child} key={child.name} />
})}
</>
)
}
function App() {
return (
<div className="App">
<RecursiveComponent {...data} />
</div>
);
}
export default App;
React components take an object as first argument: the props. When you use jsx, the attributes you pass are converted to an object.
<Component name="test" id={42} ok={true} /> // props = { name: "test", id: 42, ok: true }
Then your component receive the props like this:
const Component = (props) => {
console.log(props.name) // => "test"
// the rest of your component...
}
// Most people will use destructuration to use directly the attribute name
const Component = ({ name, id, ok }) => {
console.log(name) // => "test"
// the rest of your component...
}
The spread operator allow you to fill attributes for every properties of an object
const data = { name: "test", id: 42, ok: true }
<Component {...data} />
// is the same as :
<Component name={data.name} id={data.id} ok={data.ok} />
In your case when you pass itemsProp={data} you actually have the first argument of your component like this
const RecursiveComponent = (itemsProp) => {
// here itemsProp = { itemsProp : { name : "Level 1", children : [...] }}
// so you have to use it this way
const hasData = itemsProp.itemsProp.children && itemsProp.itemsProp.children.length;
}
you can use it with passing props <RecursiveComponent itemProps={data} /> also, see below , notice the {} brace on props while getting props in function component.
Different way 1:-
const RecursiveComponent = (props) => {
const hasData = props.itemProps.children && props.itemProps.children.length
}
you can use like above as well.
Solution 2:-
import React from "react";
const data = {
name: "Level 1",
children: [
{
name: "Level 2",
children: [
{
name: "Level 3"
}
]
},
{
name: "Level 2.2",
children: [
{
name: "Level 3.2"
}
]
}
]
};
const RecursiveComponent = ({itemProps}) => {
console.log("itemsprops", itemProps);
const hasData = itemProps.children && itemProps.children.length;
return (
<>
{itemProps.name}
{hasData && itemProps.children.map((child) => {
console.log(child);
return <RecursiveComponent itemProps={child} key={child.name} />
})}
</>
);
};
function App() {
return (
<div className="App">
<RecursiveComponent itemProps={data} />
</div>
);
}
export default App;
I'm trying to chain a React Components to Object by passing my imported Components to them as Props.
const [showComponent, setShowComponent] = useState([
{ cId: 1, componentName: <ContactDialogAddresses />, title: 'Adresse', render: true },
{ cId: 2, componentName: <ContactDialogPhone />, title: 'Rufnummer', render: true },
]);
return(
{showComponent.map((component, index) => {
if (component.render) {
return (
<>
<span>{index}</span>
{component.componentName}
</>
);
}
})}
)
How can I reimplement props like this?
// I need to pass the props during the mapping because I need a unique identifier for each rendert component later on.
<ContactDialogAddresses
key={component.cId}
onShowComponent={handleShowComponent}
cIndex={index}
/>
One of the options would be to do component creators/renderers (basically functions) in your state, rather than directly components.
Something like this:
const [showComponent, setShowComponent] = useState([
{ cId: 1, componentRenderer: (props) => { return (<ContactDialogAddresses {...props} />)}, title: 'Adresse', render: true },
{ cId: 2, componentRenderer: (props) => { return (<ContactDialogPhone {...props} />) }, title: 'Rufnummer', render: true },
]);
return(
{showComponent.map((component, index) => {
if (component.render) {
return (
<>
<span>{index}</span>
{component.componentRenderer({someProp:'someValue'})}
</>
);
}
})}
)
Another option could be using variables - important they should be capitalized (and do not do < /> in componentName in your state):
const [showComponent, setShowComponent] = useState([
{ cId: 1, componentName: ContactDialogAddresses, title: 'Adresse', render: true },
{ cId: 2, componentName: ContactDialogPhone, title: 'Rufnummer', render: true },
]);
return(
{showComponent.map((component, index) => {
if (component.render) {
const TargetComponent = component.componentName
return (
<>
<span>{index}</span>
<TargetComponent x="y"/>
</>
);
}
})}
)
Otherwise you could use createElement - https://reactjs.org/docs/react-api.html#createelement
Actually there is good thread here - React / JSX Dynamic Component Name with some good discussions there, so kudos goes there
I am using redux with my react application. I am trying to get the data from my reducer but when I am trying to do this. I am getting some error.
Uncaught Error: Given action "RECEIVE_CATEGORY_NAME", reducer
"categoriesReducer" returned undefined. To ignore an action, you must
explicitly return the previous state. If you want this reducer to hold
no value, you can return null instead of undefined.
the logic written is working fine in case of influencersNameReducer but is showing an error for categoriesReducer
home_reducer.js
import { RECEIVE_INFLUENCERS_NAME, RECEIVE_CATEGORY_NAME } from './home_actions';
export const influencersNameReducer = (state = [], { type, influencers }) => {
console.log(influencers)
return type === RECEIVE_INFLUENCERS_NAME ? influencers : state
}
export const categoriesReducer = (state = [], { type, category }) => {
console.log(type, category)
return type === RECEIVE_CATEGORY_NAME ? category : state
}
home_actions.js
export const RECEIVE_INFLUENCERS_NAME = 'RECEIVE_INFLUENCERS_NAME'
export const RECEIVE_CATEGORY_NAME = 'RECEIVE_CATEGORY_NAME';
const receiveInfluencersName = influencers => ({ type: RECEIVE_INFLUENCERS_NAME, influencers })
const receiveCategoryName = categories => ({ type: RECEIVE_CATEGORY_NAME, categories })
export const fetchInfluencers = _ => dispatch => {
$.ajax({
method: 'get',
url: 'vip_api/influencers',
data: { name: _ },
success(influencers) {
dispatch(receiveInfluencersName(influencers))
},
error({ responseJSON, statusText }) {
dispatch(receiveServerErrors(responseJSON || [statusText]))
}
})
}
export const fetchCategories = _ => dispatch => {
$.ajax({
method: 'get',
url: 'vip_api/categories',
data: { name: _ },
success(categories) {
dispatch(receiveCategoryName(categories))
},
error({ responseJSON, statusText }) {
dispatch(receiveServerErrors(responseJSON || [statusText]))
}
})
}
store.js
import {influencersNameReducer, categoriesReducer} from './Vvip/Home/home_reducer';
import { composeWithDevTools } from 'redux-devtools-extension';
const reducer = combineReducers({
categoriesReducer,
influencersNameReducer,
})
const composeEnhancers = composeWithDevTools({
// Specify name here, actionsBlacklist, actionsCreators and other options if needed
});
export default (state = {}) => (
createStore(reducer, state, composeEnhancers(applyMiddleware(errorMiddleware, timeoutMiddleware, thunk)))
)
index.js
import React, { Component } from 'react'
import Select, { components } from 'react-select'
import DateRange from '../../shared/_date_range';
import moment from 'moment';
import {ethnicities, ageRanges, isoCountries} from '../../constants';
import { connect } from 'react-redux';
import {fetchInfluencers, fetchCategories} from './home_actions';
class InfluencersForm extends Component {
constructor() {
super();
this.state = {
demography: null,
dates : {
startDate: moment(),
endDate: moment()
},
influencersName: [],
}
}
handleInfluencerName = event => {
this.props.dispatch(fetchInfluencers(event))
}
handleSelectedInfluencer = event => {
console.log(event)
this.setState({
isMenuOpenInfluencer : false
})
}
componentWillReceiveProps(newProps) {
console.log(newProps);
if (newProps.influencersNameReducer && newProps.influencersNameReducer.length) {
this.setState({
influencersName: newProps.influencersNameReducer.map((influencer, index) => {
return ({ value: influencer, label: influencer })
}),
})
}
}
handleInfluencerType = event => {
console.log(event)
}
handleInfluencerCountry = event => {
console.log(event)
}
handleInfluencerSubscribers = event => {
console.log(event)
}
handleInfluencerVideosCreated = event => {
console.log(event)
}
handleInfluencerCategory = event => {
console.log(event)
this.props.dispatch(fetchCategories(event))
}
onDemographyChange = event => {
console.log(event.currentTarget.value)
this.setState({
demography: event.currentTarget.value
})
}
handleInfluencerAge = event => {
console.log(event)
}
handleInfluencerGender = event => {
console.log(event)
}
handleInfluencerEthnicity = event => {
console.log(event)
}
updateDates = event => {
console.log(event)
this.setState({
dates: event
})
}
render() {
const influencersType = [
{ value: 'a', label: 'Type A' },
{ value: 'b', label: 'Type B' },
{ value: 'c', label: 'Type C' }
]
const influencersCategory = [
{ value: 'a', label: 'Type A' },
{ value: 'b', label: 'Type B' },
{ value: 'c', label: 'Type C' }
]
const influencersAge = ageRanges.map(age => ({ value: age, label: age }))
const influencersGender = [
{ value: 'male', label: 'Male' },
{ value: 'female', label: 'Female' }
]
const influencersKeywords = [
{ value: 'youtuber', label: 'Youtuber' },
{ value: 'vlogger', label: 'Vlogger' }
]
const influencersCountry = Object.keys(isoCountries).map(code => ({ value: code, label: isoCountries[code] }))
const DropdownIndicator = (props) => {
return components.DropdownIndicator && (
<components.DropdownIndicator {...props}>
<i className="fa fa-search" aria-hidden="true" style={{ position: 'initial', color: 'black' }}></i>
</components.DropdownIndicator>
);
};
return (
<div className='home-forms influencer-form'>
<div className='display-flex'>
<Select
options={this.state.influencersName}
onChange={this.handleSelectedInfluencer}
closeMenuOnSelect = {true}
isSearchable={true}
components={{ DropdownIndicator }}
onInputChange = {this.handleInfluencerName}
placeholder={'Start Typing Influencers Name'}
classNamePrefix="vyrill"
className="influencers influencers-icon-name" />
<Select
options={influencersType}
onChange={this.handleInfluencerType}
placeholder='Type of Influencers'
classNamePrefix="vyrill"
className="influencers influencers-icon-type" />
<Select
options={influencersCountry}
onChange={this.handleInfluencerCountry}
isSearchable={true}
components={{ DropdownIndicator }}
placeholder='Start Typing Country'
classNamePrefix="vyrill"
className="influencers influencers-icon-country" />
</div>
<div className='display-flex' style={{ marginTop: 32 }}>
<Select
options={influencersType}
onChange={this.handleInfluencerSubscribers}
placeholder='Number of Subscribers'
classNamePrefix="vyrill"
className="influencers influencers-icon-type" />
<Select
options={influencersType}
onChange={this.handleInfluencerVideosCreated}
placeholder='Number of Videos Created'
classNamePrefix="vyrill"
className="influencers influencers-icon-videos-created" />
<Select
options={influencersCategory}
onChange={this.handleInfluencerCategory}
onInputChange = {this.handleInfluencerCategory}
isSearchable={true}
components={{ DropdownIndicator }}
placeholder='Start Typing Category'
classNamePrefix="vyrill"
className="influencers influencers-icon-country influencers-icon-category" /> {/* remove influencers-icon-country later */}
</div>
<div style={{ marginTop: 50 }}>
<div className="display-flex">
<div className="icon-subscribers" style={{ marginTop: 4 }}></div>
<div style={{ fontWeight: 700, marginTop: 4 }}>Demographics</div>
<div className="radio-container">
<label>
<div style={{ fontSize: 14, marginTop: 4 }}>By influencers</div>
<input
type="radio"
name="demographics"
value="influencers"
checked={this.state.demography === 'influencers'}
onChange={this.onDemographyChange} />
<span className="custom-radio">
</span>
</label>
</div>
<div className="radio-container">
<label>
<div style={{ fontSize: 14, marginTop: 4 }}>By people in videos</div>
<input
type="radio"
name="demographics"
value="people in videos"
checked={this.state.demography === 'people in videos'}
onChange={this.onDemographyChange} />
<span className="custom-radio"></span>
</label>
</div>
</div>
</div>
<div className="display-flex" style={{ marginTop: 40 }}>
<Select
options={influencersAge}
onChange={this.handleInfluencerAge}
placeholder='Age'
classNamePrefix="vyrill"
className="influencers" />
<Select
options={influencersGender}
onChange={this.handleInfluencerGender}
placeholder='Gender'
classNamePrefix="vyrill"
className="influencers" />
<Select
options={ethnicities}
onChange={this.handleInfluencerEthnicity}
placeholder='Ethnicity'
classNamePrefix="vyrill"
className="influencers" />
</div>
<div style={{marginTop: 50}}>
<div style={{display: 'inline'}}>Contains keywords (in transcript):</div>
<span className="icon-info"></span>
<Select
options={influencersKeywords}
onChange={this.handleInfluencerName}
isSearchable={true}
classNamePrefix="vyrill"
placeholder= {" "}
className="influencers influencers-keywords"
styles = {{marginTop: 10}}/>
</div>
<div style={{marginTop: 50}} className="date-picker">
<div>Posted content time range</div>
<DateRange dates={ this.state.dates } updateDates={ this.updateDates }/>
<div className="icon-arrow-right"></div>
</div>
</div>
)
}
}
const mapStateToProps = ({ influencersNameReducer, categoriesReducer }) => ({
influencersNameReducer,
categoriesReducer
})
export default connect(mapStateToProps)(InfluencersForm)
You need to modify your reducer as:
export const influencersNameReducer = (state = [], { type, influencers }) => {
switch(type) {
case RECEIVE_INFLUENCERS_NAME:
return influencers;
default:
return state;
}
}
export const categoriesReducer = (state = [], { type, category }) => {
switch(type) {
case RECEIVE_CATEGORY_NAME:
return category;
default:
return state;
}
}
On every action the dispatcher goes to every reducer. Since in your code the influencersNameReducer reducer was not doing anything for type RECEIVE_CATEGORY_NAME thus returning undefined. So you were getting the error. Using switch case is the way to do this.
constructor(props) {
super(props);
this.state = {
posts: [],
loading: true
};
}
componentDidMount() {
axios.get('/posts')
.then(response => {
console.log('---');
console.log(response.data);
console.log('---');
this.setState({ posts: response.data, loading: false });
});
}
toggleItem(index) {
console.log('clicked index: '+index);
}
render () {
let content;
if (this.state.loading) {
content = 'Loading...';
} else {
content = this.state.posts.map(post => {
return(
<li key={post.id} className={}>
<div>
<Moment format="MMM DD # HH:MM">
<span className="badge badge-pill badge-primary">{post.created_at}</span>
</Moment>
<button className="btn btn-primary btn-sm" onClick={this.toggleItem.bind(this, post.id)}>Toggle Message</button>
</div>
<div className="msg">{post.message}</div>
</li>
)
});
}
return (
<div>
<h1>Posts!</h1>
<div className="row">
<div className="col-md-6">
{content}
</div>
<div className="col-md-6">
x
</div>
</div>
</div>
);
What I am trying to achieve - when someone clicks the button, I want to toggle (show or hide) the respective .msg.
Where I struggle - I would like to default hide all the messages and when the button is clicked, then to display the respective msg. But I am not sure how to do it in React - one thought is to hide them in default with using CSS and then to create a new state for the clicked item?
Or should I pre-create an array of states for monitoring all messages?
You can add CSS classes or styling conditionally based on a Boolean key of each item.
When an item gets clicked, just flip the Boolean value.
Here is a small running example:
class Item extends React.Component {
onClick = () => {
const { onClick, id } = this.props;
onClick(id);
}
render() {
const { name, active } = this.props;
return (
<div
onClick={this.onClick}
style={{ opacity: active ? '1' : '0.4' }}
>
{name}
</div>
);
}
}
class App extends React.Component {
state = {
items: [
{ name: 'Item 1', id: 1 },
{ name: 'Item 2', id: 2 },
{ name: 'Item 3', id: 3 },
{ name: 'Item 4', id: 4 },
{ name: 'Item 5', id: 5 },
]
}
onItemClick = id => {
this.setState(prev => {
const { items } = prev;
const nexItems = items.map(item => {
if (item.id !== id) return item;
return {
...item,
active: !item.active
}
});
return { ...prev, items: nexItems };
});
}
render() {
const { items } = this.state;
return (
<div>
{
items.map(item => (
<Item key={item.id} {...item} onClick={this.onItemClick} />
))
}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>