I have one Component which shows a list of data in a dropdown and there is an option to search these data which works as a filter. Here is my code:
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Popover from '../../Popover';
import Input from '../../Input';
import Icon from '../../Icon';
import IconButton from '../../IconButton';
const DropDownFilter = props => {
const { label, options, onChange, isSearchEnabled } = props;
const [activeOption, setActiveOption] = useState({});
const [filter, setfilter] = useState('');
const searchFilter = event => {
setfilter(event.target.value);
};
const removeFilter = () => {
setfilter('');
};
const lowercasedFilter = filter.toLowerCase();
const filteredData = options.filter(item => {
return Object.keys(item).some(
key => typeof item[key] === 'string' && item[key].toLowerCase().includes(lowercasedFilter)
);
});
const labelText = activeOption.label ? activeOption.label : label;
const handleSelectedOption = option => {
setActiveOption(option);
onChange(option);
};
return (
<div className="filter">
<Popover linkText={labelText} size="small" direction="bottom-left">
{isSearchEnabled && (
<div className="filter__search">
<Input
value={filter}
onChange={searchFilter}
preIcon={
<div role="presentation">
<Icon name="search" />
</div>
}
placeholder="Search"
postIcon={
filter.length > 0 && (
<IconButton
icon={<Icon name="close" />}
size="tiny"
onClick={removeFilter}
standalone={true}
isIconOnly={true}
/>
)
}
/>
</div>
)}
<ul className="filter__options filter__options--scrollbar">
{filteredData.map(option => (
<li
key={option.value}
role="presentation"
className={classNames('filter__options-option', {
'filter__options-option--active': option.value === activeOption.value,
})}
onClick={() => handleSelectedOption(option)}
>
{option.label}
</li>
))}
</ul>
</Popover>
</div>
);
};
DropDownFilter.defaultProps = {
label: 'Filter Menu',
options: [],
isSearchEnabled: true,
};
DropDownFilter.propTypes = {
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
options: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
})
),
onChange: PropTypes.func.isRequired,
isSearchEnabled: PropTypes.bool,
};
export default DropDownFilter;
Here is a gif of it: https://recordit.co/HtalUtuPsj
Now during searching I want to send the value of the search param to another component, the value will be used to search from a DB or any other external data source which is being handled in that new component. Such as, if I am searching for Ratings, this component should search for it in the existing options list it has in its own component, as well as the same time it will search for Ratings in any other external data source or DB. This external network call, search or any other functionality will be processed in the other component. So this component will only send the search param; for example Ratings to the other component in real time.
I can think of an idea like I will get the searchParam in a state and pass the setState value to a new props which will be called through an onSearchParamChange function, this new function will pass the data through a callback and the other component will get the data through calling that props of this component. I am not sure if this is the correct way and also I am not able to implement this thought in the code either. Is there any better way to do it? if so what would be that coding implementation?
If you need to pass to a parent component you should be able to use for example the onChange prop which is passed to your component, like you are doing in the handleSelectedOption function. That function is in fact passing the chosen option to the parent component. If you want to pass to the parent component when the user is typing, then you should call the onChange function also in searchFilter:
const searchFilter = event => {
const option = event.target.value);
setfilter(option);
onChange(option);
};
If you want to pass it to a child component, the you can just pass it as prop:
<ChildComponent filter={ filter } />
Related
I have a seemingly trivial question about props and function components. Basically, I have a container component which renders a Modal component upon state change which is triggered by user click on a button. The modal is a stateless function component that houses some input fields which need to connect to functions living inside the container component.
My question: How can I use the functions living inside the parent component to change state while the user is interacting with form fields inside the stateless Modal component? Am I passing down props incorrectly?
Container
export default class LookupForm extends Component {
constructor(props) {
super(props);
this.state = {
showModal: false
};
}
render() {
let close = () => this.setState({ showModal: false });
return (
... // other JSX syntax
<CreateProfile fields={this.props} show={this.state.showModal} onHide={close} />
);
}
firstNameChange(e) {
Actions.firstNameChange(e.target.value);
}
};
Function (Modal) Component
const CreateProfile = ({ fields }) => {
console.log(fields);
return (
... // other JSX syntax
<Modal.Body>
<Panel>
<div className="entry-form">
<FormGroup>
<ControlLabel>First Name</ControlLabel>
<FormControl type="text"
onChange={fields.firstNameChange} placeholder="Jane"
/>
</FormGroup>
);
};
Example: say I want to call this.firstNameChange from within the Modal component. I guess the "destructuring" syntax of passing props to a function component has got me a bit confused. i.e:
const SomeComponent = ({ someProps }) = > { // ... };
You would need to pass down each prop individually for each function that you needed to call
<CreateProfile
onFirstNameChange={this.firstNameChange}
onHide={close}
show={this.state.showModal}
/>
and then in the CreateProfile component you can either do
const CreateProfile = ({onFirstNameChange, onHide, show }) => {...}
with destructuring it will assign the matching property names/values to the passed in variables. The names just have to match with the properties
or just do
const CreateProfile = (props) => {...}
and in each place call props.onHide or whatever prop you are trying to access.
I'm using react function component
In parent component first pass the props like below shown
import React, { useState } from 'react';
import './App.css';
import Todo from './components/Todo'
function App() {
const [todos, setTodos] = useState([
{
id: 1,
title: 'This is first list'
},
{
id: 2,
title: 'This is second list'
},
{
id: 3,
title: 'This is third list'
},
]);
return (
<div className="App">
<h1></h1>
<Todo todos={todos}/> //This is how i'm passing props in parent component
</div>
);
}
export default App;
Then use the props in child component like below shown
function Todo(props) {
return (
<div>
{props.todos.map(todo => { // using props in child component and looping
return (
<h1>{todo.title}</h1>
)
})}
</div>
);
}
An addition to the above answer.
If React complains about any of your passed props being undefined, then you will need to destructure those props with default values (common if passing functions, arrays or object literals) e.g.
const CreateProfile = ({
// defined as a default function
onFirstNameChange = f => f,
onHide,
// set default as `false` since it's the passed value
show = false
}) => {...}
just do this on source component
<MyDocument selectedQuestionData = {this.state.selectedQuestionAnswer} />
then do this on destination component
const MyDocument = (props) => (
console.log(props.selectedQuestionData)
);
A variation of finalfreq's answer
You can pass some props individually and all parent props if you really want (not recommended, but sometimes convenient)
<CreateProfile
{...this.props}
show={this.state.showModal}
/>
and then in the CreateProfile component you can just do
const CreateProfile = (props) => {
and destruct props individually
const {onFirstNameChange, onHide, show }=props;
I have a parent component which contains 4 buttons which I used to set a payment state. I want to pass the payment state to a a text field child component and also make it possible to use the text field to edit the state.
I have tried to use something like this how to update parent state in child component and pass to parent state back? which works but I am using formik with yup validation in parent component and it does not seem to detect the change of value from the text field child component.
Parent Component:
import React from "react";
import Child from 'path';
const animals = {
{
key:'1',
label:'Elephant',
isDefault:false
},
{
key:'2',
label:'Lion',
isDefault:false
},
{
key:'3',
label:'Bear',
isDefault:false
},
}
const Parent = () =>{
const defaultAnimalBtn = animals.some(choice => choice.isDefault === true);
const [ animalState, setAnimalState ] = useState({value: defaultAnimalBtn.label});
const handleAnimal = (index) => {
const selectedAnimal = animalState[index];
setAnimalState({value: selectedAnimal.label});
};
const handleAnimalChange = (event)=>{
event.preventDefault();
setAnimalState({value: event.target.value});
};
return(
<>
{
animals.map(({ key, label }, index)=>(
<Button
key={key}
variant={`${label === animalState.value ? 'contained':'outlined'}`}
onClick={() => handlePayment(index)}
>
{label}
</Button>
))
}
<Child
id="animal"
name="animal"
label="Animal"
amount={animalState.value}
onChange={handleAnimalChange}
/>
</>
);
}
Child Component:
import React from "react";
import { Field, useField } from 'formik';
const Child = ({ id, label, ...props })=>{
const [ field, meta ] = useField(props);
return(
<Field component={FormControl} fullWidth sx={{ mt: 3}} variant="standard">
<InputLabel htmlFor={id}>{label}</InputLabel>
<Input
id={id}
name={props.name}
onChange={props.onChange}
value={props.amount}
{ ...props }
startAdornment={<InputAdornment position="start">$</InputAdornment>}
label={label}
/>
{_renderHelperText()}
</Field >
);
}
export default Child;
Note: I am using React MUI as my component library. I am also rendering the Parent component inside a custom MultiStepForm using Formik and yup for form validation.
I followed this tutorial(Video:Formik Multi-Step Form with Material UI) to create the MultiStepForm.
EDIT: Link to Sandbox.
You shouldn't modify your parent state in your child, instead you should pass a function that modifies the data from parent to child and calling it as needed
So here's the user function I'm trying to create:
1.) User double clicks on text
2.) Text turns into input field where user can edit text
3.) User hits enter, and upon submission, text is updated to be edited text.
Basically, it's just an edit function where the user can change certain blocks of text.
So here's my problem - I can turn the text into an input field upon a double click, but how do I get the edited text submitted and rendered?
My parent component, App.js, stores the function to update the App state (updateHandler). The updated information needs to be passed from the Tasks.jsx component, which is where the text input is being handled. I should also point out that some props are being sent to Tasks via TaskList. Code as follows:
App.js
import React, {useState} from 'react';
import Header from './Header'
import Card from './Card'
import cardData from './cardData'
import Dates from './Dates'
import Tasks from './Tasks'
import Footer from './Footer'
import TaskList from './TaskList'
const jobItems= [
{
id:8,
chore: 'wash dishes'
},
{
id:9,
chore: 'do laundry'
},
{
id:10,
chore: 'clean bathroom'
}
]
function App() {
const [listOfTasks, setTasks] = useState(jobItems)
const updateHandler = (task) => {
setTasks(listOfTasks.map(item => {
if(item.id === task.id) {
return {
...item,
chore: task.chore
}
} else {
return task
}
}))
}
const cardComponents = cardData.map(card => {
return <Card key = {card.id} name = {card.name}/>
})
return (
<div>
<Header/>
<Dates/>
<div className = 'card-container'>
{cardComponents}
</div>
<TaskList jobItems = {listOfTasks} setTasks = {setTasks} updateHandler = {updateHandler}/>
<div>
<Footer/>
</div>
</div>
)
}
export default App;
Tasks.jsx
import React, {useState} from 'react'
function Tasks (props) {
const [isEditing, setIsEditing] = useState(false)
return(
<div className = 'tasks-container'>
{
isEditing ?
<form>
<input type = 'text' defaultValue = {props.item.chore}/>
</form>
: <h1 onDoubleClick ={()=> setIsEditing(true)}>{props.item.chore}</h1>
}
</div>
)
}
export default Tasks
TaskList.jsx
import React from 'react'
import Tasks from './Tasks'
function TaskList (props) {
const settingTasks = props.setTasks //might need 'this'
return (
<div>
{
props.jobItems.map(item => {
return <Tasks key = {item.id} item = {item} setTasks = {settingTasks} jobItems ={props.jobItems} updateHandler = {props.updateHandler}/>
})
}
</div>
)
}
export default TaskList
You forgot onChange handler on input element to set item's chore value.
Tasks.jsx must be like below
import React, {useState} from 'react'
function Tasks (props) {
const [isEditing, setIsEditing] = useState(false)
const handleInputChange = (e)=>{
// console.log( e.target.value );
// your awesome stuffs goes here
}
return(
<div className = 'tasks-container'>
{
isEditing ?
<form>
<input type = 'text' onChange={handleInputChange} defaultValue = {props.item.chore}/>
</form>
: <h1 onDoubleClick ={()=> setIsEditing(true)}>{props.item.chore}</h1>
}
</div>
)
}
export default Tasks
So, first of all, I would encourage you not to switch between input fields and divs but rather to use a contenteditable div. Then you just use the onInput attribute to call a setState function, like this:
function Tasks ({item}) {
return(
<div className = 'tasks-container'>
<div contenteditable="true" onInput={e => editTask(item.id, e.currentTarget.textContent)} >
{item.chore}
</div>
</div>
)
}
Then, in the parent component, you can define editTask to be a function that find an item by its id and replaces it with the new content (in a copy of the original tasks array, not the original array itself.
Additionally, you should avoid renaming the variable between components. (listOfTasks -> jobItems). This adds needless overhead, and you'll inevitably get confused at some point which variable is connected to which. Instead say, <MyComponent jobItems={jobItems} > or if you want to allow for greater abstraction <MyComponent items={jobItems} > and then you can reuse the component for listable items other than jobs.
See sandbox for working example:
https://codesandbox.io/s/practical-lewin-sxoys?file=/src/App.js
Your Task component needs a keyPress handler to set isEditing to false when enter is pressed:
const handleKeyPress = (e) => {
if (e.key === "Enter") {
setIsEditing(false);
}
};
Your updateHandler should also be passed to the input's onChange attribute, and instead of defaultValue, use value. It also needs to be reconfigured to take in the onChange event, and you can map tasks with an index to find them in state:
const updateHandler = (e, index) => {
const value = e.target.value;
setTasks(state => [
...state.slice(0, index),
{ ...state[index], chore: value },
...state.slice(index + 1)
]);
};
Finally, TaskList seems like an unnecessary middleman since all the functionality is between App and Task; you can just render the tasks directly into a div with a className of your choosing.
react-edit-text is a package I created which does exactly what you described.
It provides a lightweight editable text component in React.
A live demo is also available.
Some time ago I tried to make my first steps in React. Now I'm trying to create simple ToDo list app. One of my colleagues told me that it will be nice to use PropTypes with shape and arrayOf. I have one component which pass data to another and here is my issue(List.js and ListItem.js). I'm not sure when, where and how to use propTypes with shape and arrayOf. Which approach is right? Define PropTypes in parent component or in child component? Maybe there is a better idea? Please find my code below, it will be nice if you can clarify me how to use PropTypes. I tried to search something in Web, but I still don't get it.
Main App.js file:
import React from "react";
import "./styles/App.scss";
import List from "./components/List/List";
import AppHeader from "./components/AppHeader/AppHeader";
function App() {
return (
<div className="App">
<AppHeader content="TODO List"></AppHeader>
<List></List>
</div>
);
}
export default App;
AppHeader.js file:
import React from "react";
import "./AppHeader.scss";
export default function AppHeader(props) {
return (
<div className="header">
<h1 className="header-headline">{props.content}</h1>
</div>
);
}
List.js file:
import React from "react";
import "./List.scss";
import ListItem from "../ListItem/ListItem";
import _ from "lodash";
const initialList = [
{ id: "a", name: "Water plants", status: "done" },
{ id: "b", name: "Buy something to eat", status: "in-progress" },
{ id: "c", name: "Book flight", status: "in-preparation" }
];
const inputId = _.uniqueId("form-input-");
// function component
const List = () => {
const [value, setValue] = React.useState(""); // Hook
const [list, setList] = React.useState(initialList); // Hook
const handleChange = event => {
setValue(event.target.value);
};
// add element to the list
const handleSubmit = event => {
// prevent to add empty list elements
if (value) {
setList(
list.concat({ id: Date.now(), name: value, status: "in-preparation" })
);
}
// clear input value after added new element to the list
setValue("");
event.preventDefault();
};
// remove current element from the list
const removeListItem = id => {
setList(list.filter(item => item.id !== id));
};
// adding status to current element of the list
const setListItemStatus = (id, status) => {
setList(
list.map(item => (item.id === id ? { ...item, status: status } : item))
);
};
return (
<div className="to-do-list-wrapper">
<form className="to-do-form" onSubmit={handleSubmit}>
<label htmlFor={inputId} className="to-do-form-label">
Type item name:
</label>
<input
type="text"
value={value}
onChange={handleChange}
className="to-do-form-input"
id={inputId}
/>
<button type="submit" className="button to-do-form-button">
Add Item
</button>
</form>
<ul className="to-do-list">
{list.map(item => (
<ListItem
name={item.name}
status={item.status}
key={item.id}
id={item.id}
setListItemStatus={setListItemStatus}
removeListItem={removeListItem}
></ListItem>
))}
</ul>
</div>
);
};
export default List;
And ListItem.js file:
import React from "react";
import PropTypes from "prop-types";
import "./ListItem.scss";
const ListItem = props => {
return (
<li className={"list-item " + props.status} key={props.id}>
<span className="list-item-icon"></span>
<div className="list-item-content-wrapper">
<span className="list-item-text">{props.name}</span>
<div className="list-item-button-wrapper">
<button
type="button"
onClick={() => props.setListItemStatus(props.id, "in-preparation")}
className="button list-item-button"
>
In preparation
</button>
<button
type="button"
onClick={() => props.setListItemStatus(props.id, "in-progress")}
className="button list-item-button"
>
In progress
</button>
<button
type="button"
onClick={() => props.setListItemStatus(props.id, "done")}
className="button list-item-button"
>
Done
</button>
<button
type="button"
onClick={() => props.removeListItem(props.id)}
className="button list-item-button"
>
Remove
</button>
</div>
</div>
</li>
);
};
ListItem.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
status: PropTypes.string,
removeListItem: PropTypes.func,
setListItemStatus: PropTypes.func
};
export default ListItem;
Props are per component.
The purpose of prop types is to let you make some type checking on a component's props.
It seems like you did it correctly in the ListItem component.
Basically, you just list all of the component's props and what type they should be.
Like you did here:
ListItem.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
status: PropTypes.string,
removeListItem: PropTypes.func,
setListItemStatus: PropTypes.func
};
I am not exactly sure where will the propTypes shape and arrayOf be used, but generally, PropTypes are useful when you are trying to render a child component within a parent component, and you want to carry out TypeChecking on the props of the relative child component.
In your scenario, arrayOf and shape can be used in one of the props within your component whereby the prop is an array of a certain type (arrayOf), and an object of a certain type(shape, or exact).
I have a tricky situation with react router 4.
Imagine I have a route
<Route path='/search/:term?' render={(props) => {
return (<ProductList
setFlag={(flag)=>this.setState({flag})}
{...props}
/>)
}} />
Now you can see I am using render in Route which means it will not unmount this component at each render rather update the old instance with new props.
However, at some point inside the ProductList the user calls setFlag function which you can see updates some property in parent.
Because of this, a rerender of the parent is caused. Which also calls componentWillReceiveProps(CWRP) of ProductList. Inside CWRP of ProductList I am always (unconditionally) fetching items with new props.
This causes my problem. You can see that when user updated flag, there was no need to fetch data again in CWRP, because updating that flag wasn't related to my data.
You could say that I should put some condition in CWRP that would do some check and fetch data only when it is necessary. However, I find it impossible to come up with such check. Because for example, ProductList receives a search term. I could, for example, compare a search term from the previous render to search term of new render and if they are different then to fetch data, however, that is incorrect, because even in case of same search term a fetch should be issued (maybe the data was updated on a server).
What solution do you see in such a situation?
So that my product list doesn't fetch data everytime the flag of parent changes?
Elevate your state and move your logic out of the render method and into a parent container-component, then utilize this.setState() to stop state updates OR use shouldComponentUpdate() to continue to allow state updates BUT stop re-renders when the flag hasn't been changed (either one will prevent ProductList from being updated):
import React, { Component } from 'react';
import ProductList from './ProductList';
export default class SearchTerms extends Component {
state = { flag: '' };
shouldComponentUpdate = (nextProps, nextState) => ( this.state.flag !== nextState.flag )
handleFlag = flag => this.setState(prevState => { return this.state.flag !== flag ? { flag } : null })
render = () => ( <ProductList setFlag={this.handleFlag} {...this.state} {...this.props} /> )
}
Then the route will change to:
<Route path='/search/:term?' component={SearchTerms} />
In addition, I'd avoid using componentWillReceiveProps() altogether and instead use componentDidUpdate().
Here's an example of a parent container-component controlling several component children. The children can update the parent via a passed down parent method.
In this simple example, searchForPlayer's onChange and onSubmit updates the parent's searchTerm state and changes the URL query via parent's handleSubmit method. The URL query change triggers the parent's componentDidUpdate method, which then fetches new data and updates the displayPlayerList component.
URL before:
/players/all
URL after form submit:
/players/player?number=${this.state.searchTerm}
So if a user types out the URL to:
/players/player?number=10
or
/players/fdskmsdfk?number=10
and hits enter, it'll load a filtered list because it's only looking for a number query.
If they go to:
/player/dsfdsdfdsdf
or
player/1223345
or anything without a number query, then it'll just fetch all the players instead (this can be handled differently, but was done for simplicity).
Working example: https://codesandbox.io/s/xn3p3o6vq
containers/PlayersList.js (parent container-component)
import isEmpty from "lodash/isEmpty";
import React, { Component, Fragment } from "react";
import qs from "qs";
import DisplayPlayerList from "../components/displayPlayerList";
import NoPlayerFound from "../components/noPlayerFound";
import SearchForPlayer from "../components/searchForPlayer";
import ServerError from "../components/serverError";
import Spinner from "../components/spinner";
export default class PlayersList extends Component {
state = {
err: "",
isLoading: true,
searchTerm: "",
players: [],
noplayer: "",
number: ""
};
componentDidMount = () => this.fetchPlayers();
componentDidUpdate = (prevProps, prevState) => this.props.location.search !== prevProps.location.search && this.fetchPlayers();
fetchPlayers = () => {
const { number } = qs.parse(this.props.location.search, { ignoreQueryPrefix: true })
fetch(`https://jsonplaceholder.typicode.com/users${number ? `/${number}` : ""}`)
.then(response => response.json())
.then(players =>
this.setState({
err: "",
players: !number ? [...players] : [players],
noplayer: isEmpty(players) ? true : false,
isLoading: false,
number,
searchTerm: ""
})
)
.catch(err => this.setState({ err: err.toString() }));
};
handleChange = e => this.setState({ searchTerm: e.target.value });
handleSubmit = e => {
e.preventDefault();
this.props.history.push(`/players/player?number=${this.state.searchTerm}`);
};
render = () => (
this.state.isLoading // (if isLoading is true..)
? <Spinner /> // (then show a spinner)
: <div style={{ padding: 20, width: 500 }}> // (otherwise...)
<SearchForPlayer // (show player search form and...)
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
{...this.state}
/>
{ this.state.err // (if there's an error...)
? <ServerError {...this.state} /> // (show the error)
: this.state.noplayer // (otherwise, if there's no player...)
? <NoPlayerFound {...this.state} /> // (show no player found)
: <DisplayPlayerList {...this.state} /> // (otherwise, display updated list)
}
</div>
);
}
components/searchForPlayer.js (child component)
import React from "react";
export default ({ handleChange, handleSubmit, searchTerm }) => (
<form onSubmit={handleSubmit}>
<input
className="uk-input"
type="number"
value={searchTerm}
onChange={handleChange}
placeholder="Search for player by number..."
style={{ width: 300, marginRight: 10 }}
min={1}
/>
<button
disabled={!searchTerm}
className="uk-button uk-button-primary"
type="submit"
>
Search
</button>
</form>
);
components/displayPlayerList.js (child component)
import map from "lodash/map";
import React from "react";
export default ({ players }) => (
<ul style={{ listStyleType: "none" }}>
{map(players, ({ id, name, username, email }) => (
<li style={{ margin: "10px 0" }} key={id}>
<strong>Player # {id}</strong>
<span> - {name}</span>
</li>
))}
</ul>
);
components/noPlayerFound.js (child component)
import React from "react";
export default ({ number }) => (
<div style={{ color: "red", padding: 20 }}>
No player was found matching #{number}!
</div>
);
component/serverError.js (child component)
import React from "react";
export default ({ err }) => (
<div style={{ color: "red", padding: 20 }}>
<i style={{ marginRight: 5 }} className="fas fa-exclamation-circle" /> {err}
</div>
);