I'm using a HOC component to bind an action to many different types of element, including SVG cells, which, when an onClick is bound normally, it works, but when I use my HOC it returns un-intended results.
Minimally reproducible example: https://codesandbox.io/s/ecstatic-keldysh-3viw0
The HOC component:
export const withReport = Component => ({ children, ...props }) => {
console.log(Component); //this only prints for ListItem elements for some reason
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
console.log('clicked!'); //even this wont work on some.
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}>
{children}
</Component>
);
};
Usage working:
const ListItemWIthReport = withReport(ListItem); //list item from react-mui
{items.map((item, key) => (
<ListItemWithReport report={item.report} key={key} button>
{/* listitem children*/}
</ListItemWithReport>
))}
Usage not working:
const BarWithReport = withReport(Bar); //Bar from recharts
{bars.map((bar, index) => (
<BarWithReport
report={bar.report}
key={index}
dataKey={bar.name}
fill={bar.fill}
/>
))}
The ListItem works 100% as anticipated, however, the bars will not render inside of the BarChart. Similarly, with a PieChart the Cells will actually render, with the correct sizes according to their values, however, props like "fill" do not appear to pass down.
Am I using the HOC incorrectly? I don't see an option other than HOC for the inside of Charts as many types of elements will be considered invalid HTML?
You might be dealing with components that have important static properties that need to be hoisted into the wrapped component or need to have ref forwarding implemented in order for their parent components to handle them. Getting these pieces in place is important, especially when wrapping components where you don't know their internals. That Bar component, for example, does have some static properties. Your HOC is making those disappear.
Here's how you can hoist these static members:
import hoistNonReactStatic from 'hoist-non-react-statics';
export const withReport = Component => {
const EnhancedComponent = props => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}/>
);
};
hoistNonReactStatic(EnhancedComponent, Component);
return EnhancedComponent;
};
Docs on hoisting statics and ref forwarding can be found in this handy guide to HOCs.
There may be some libraries that can take care of all these details for you. One, addhoc, works like this:
import addHOC from 'addhoc';
export const withReport = addHOC(render => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return render({ onClick: handleClick });
});
Of course, if the parent component is checking child components by type explicitly, then you won't be able to use HOCs at all. In fact, it looks like recharts has that issue. Here you can see the chart is defined in terms of child components which are then searched for explicitly by type.
I think your HOC is invalid, because not every wrapper-Component (e.g. HTML element) is basically clickable. Maybe this snipped can clarify what I am trying to say:
const withReport = Component => (props) => {
const handleClick = () => console.log('whatever')
// Careful - your component might not support onClick by default
return <Component onClick={handleClick} {...props} />
// vs.
return <div onClick={handleClick} style={{backgroundColor: 'green'}}>
<Component {...props} />
{props.children}
</div>
}
// Your import from wherever you want
class SomeClass extends React.Component {
render() {
return <span onClick={this.props.onClick}>{this.props.children}</span>
// vs.
return <span style={{backgroundColor: 'red'}}>
{
// Careful - your imported component might not support children by default
this.props.children
}
</span>
}
}
const ReportedListItem = withReport(SomeClass)
ReactDOM.render(<ReportedListItem>
<h2>child</h2>
</ReportedListItem>, mountNode)
You can have the uppers or the unders (separated by vs.) but not crossed. The HOC using the second return (controlled wrapper-Component) is sure more save.
I've used 4 methods successfully to wrap Recharts components.
First Method
Wrap the component in a HOC and use Object.Assign with some overloads. This breaks some animation and difficult to use an active Dot on lines. Recharts grabs some props from components before rendering them. So if the prop isn't passed into the HOC, then it won't render properly.
...
function LineWrapper({
dataOverload,
data,
children,
strokeWidth,
strokeWidthOverload,
isAnimationActive,
dot,
dotOverload,
activeDot,
activeDotOverload,
...rest
}: PropsWithChildren<Props>) {
const defaultDotStroke = 12;
return (
<Line
aria-label="chart-line"
isAnimationActive={false}
strokeWidth={strokeWidthOverload ?? 2}
data={dataOverload?.chartData ?? data}
dot={dotOverload ?? { strokeWidth: defaultDotStroke }}
activeDot={activeDotOverload ?? { strokeWidth: defaultDotStroke + 2 }}
{...rest}
>
{children}
</Line>
);
}
export default renderChartWrapper(Line, LineWrapper, {
activeDot: <Dot r={14} />,
});
const renderChartWrapper = <P extends BP, BP = {}>(
component: React.ComponentType<BP>,
wrapperFC: React.FC<P>,
defaultProps?: Partial<P>
): React.FC<P> => {
Object.assign(wrapperFC, component);
if (defaultProps) {
wrapperFC.defaultProps = wrapperFC.defaultProps ?? {};
Object.assign(wrapperFC.defaultProps, defaultProps);
}
return wrapperFC;
};
Second Method
Use default props to assign values. Any props passed into the HOC will be overridden.
import { XAxisProps } from 'recharts';
import { createStyles } from '#material-ui/core';
import { themeExtensions } from '../../../assets/theme';
const useStyles = createStyles({
tickStyle: {
...themeExtensions.font.graphAxis,
},
});
type Props = XAxisProps;
// There is no actual implementation of XAxis. Recharts render function grabs the props only.
function XAxisWrapper(props: Props) {
return null;
}
XAxisWrapper.displayName = 'XAxis';
XAxisWrapper.defaultProps = {
allowDecimals: true,
hide: false,
orientation: 'bottom',
width: 0,
height: 30,
mirror: false,
xAxisId: 0,
type: 'category',
domain: [0, 'auto'],
padding: { left: 0, right: 0 },
allowDataOverflow: false,
scale: 'auto',
reversed: false,
allowDuplicatedCategory: false,
tick: { style: useStyles.tickStyle },
tickCount: 5,
tickLine: false,
dataKey: 'key',
};
export default XAxisWrapper;
Third Method
I didn't like this so I've worked around it, but you can extend the class.
export default class LineWrapper extends Line {
render(){
return (<Line {...this.props} />
}
}
Fourth Method
I don't have a quick example of this, but I always render the shape or children and provide functions to help. For example, for bar cells I use this:
export default function renderBarCellPattern(cellOptions: CellRenderOptions) {
const { data, fill, match, pattern } = cellOptions;
const id = _uniqueId();
const cells = data.map((d) =>
match(d) ? (
<Cell
key={`cell-${id}`}
strokeWidth={4}
stroke={fill}
fill={`url(#bar-mask-pattern-${id})`}
/>
) : (
<Cell key={`cell-${id}`} strokeWidth={2} fill={fill} />
)
);
return !pattern
? cells
: cells.concat(
<CloneElement<MaskProps>
key={`pattern-${id}`}
element={pattern}
id={`bar-mask-pattern-${id}`}
fill={fill}
/>
);
}
// and
<Bar {...requiredProps}>
{renderBarCellPattern(...cell details)}
</Bar>
CloneElement is just a personal wrapper for Reacts cloneElement().
Related
I have a dynamic component in which I pass in children as prop.
So the props look something like:
interface Props {
...some props
children: React.ReactNode
}
export default Layout({...some props, children}: Props) {...}
I need to access the size of the children elements (height and width), in the Layout component. Note that the children are from completely different components and are non-related.
I can use the Layout component as follow:
<Layout ...some props>
<Child1 /> // I need to know the height and width of this child
<Child2 /> // as well as this child
<Child3 /> // and this child.
</Layout>
How can I do so dynamically? Do I somehow have to convert ReactNode to HTMLDivElement? Note that there is no way I can pass in an array of refs as a prop into Layout. Because that the pages which use Layout are dynamically generated.
Since many doesn't really understand what I meant by dynamically generated. It means that the pages which are using the Layout component can pass in x amount of children. The amount of children is unknown but never 0.
You can achieve this by using React.Children to dynamically build up a list of references before rendering the children. If you have access to the children element references, you can follow the below approach. If you don't then you can follow the bit at the bottom.
You have access to the children element references
If the children components pass up their element reference, you can use React.Children to loop through each child and get each element reference. Then use this to perform calculations before the children components are rendered.
i.e. This is a very simple example on how to retrieve the references and use them.
interface LayoutWrapperProps {
onMount: () => void;
}
const LayoutWrapper: React.FC<LayoutWrapperProps> = ({ onMount, children }) => {
React.useEffect(() => {
onMount();
}, [onMount]);
return <>{children}</>;
};
const Layout: React.FC = ({ children }) => {
const references = React.useRef<HTMLElement[]>([]);
React.useEffect(() => {
references.current = [];
});
function getReference(ref: HTMLElement) {
references.current = references.current.filter(Boolean).concat(ref);
}
function getHeights() {
const heights = references.current.map((ref) =>
ref?.getBoundingClientRect()
);
console.log(heights);
}
const clonedChildren = React.Children.map(children, (child) => {
return React.cloneElement(child as any, {
ref: getReference
});
});
return <LayoutWrapper onMount={getHeights}>{clonedChildren}</LayoutWrapper>;
};
If you don't have access to the children element references
If the children components aren't passing up an element as the reference, you'll have to wrap the dynamic children components in a component so we can get an element reference. i.e.
const WrappedComponent = React.forwardRef((props, ref) => {
return (
<div ref={ref}>
{props.children}
</div>
)
});
When rendering the children components, then the code above that gets the references will work:
<Layout>
<WrappedComponent>
<Child1 />
</WrappedComponent>
</Layout>
Since we don't know how your children is built, here is what I can propose you :
import React from 'react';
import { render } from 'react-dom';
const App = () => {
const el1Ref = React.useRef();
const el2Ref = React.useRef();
const [childrenValues, setChildrenValues] = React.useState([]);
React.useEffect(() => {
setChildrenValues([
el1Ref.current.getBoundingClientRect(),
el2Ref.current.getBoundingClientRect()
]);
}, []);
return (
<Parent childrenVals={childrenValues}>
<span ref={el1Ref}>
<Child value="Hello" />
</span>
<span ref={el2Ref}>
<Child value="<div>Hello<br />World</div>" />
</span>
</Parent>
);
};
const Parent = ({ children, childrenVals }) => {
React.useEffect(() => {
console.log('children values from parent = ', childrenVals);
});
return <>{children}</>;
};
const Child = ({ value }) => {
return <div dangerouslySetInnerHTML={{ __html: value }} />;
};
render(<App />, document.getElementById('root'));
And here is the repro on Stackblitz.
The idea is to manipulate how your children is built.
I have an array of React components that receive props from a map function, however the issue is that the components are mounted and unmounted on any state update. This is not an issue with array keys.
Please see codesandbox link.
const example = () => {
const components = [
(props: any) => (
<LandingFirstStep
eventImage={eventImage}
safeAreaPadding={safeAreaPadding}
isActive={props.isActive}
onClick={progressToNextIndex}
/>
),
(props: any) => (
<CameraOnboarding
safeAreaPadding={safeAreaPadding}
circleSize={circleSize}
isActive={props.isActive}
onNextClick={progressToNextIndex}
/>
),
];
return (
<div>
{components.map((Comp, index) => {
const isActive = index === currentIndex;
return <Comp key={`component-key-${index}`} isActive={isActive} />;
})}
</div>
)
}
If I render them outside of the component.map like so the follow, the component persists on any state change.
<Comp1 isActive={x === y}
<Comp2 isActive={x === y}
Would love to know what I'm doing wrong here as I am baffled.
Please take a look at this Codesandbox.
I believe I am doing something wrong when declaring the array of functions that return components, as you can see, ComponentOne is re-rendered when the button is pressed, but component two is not.
You should take a look at the key property in React. It helps React to identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity
I think there are two problems:
To get React to reuse them efficiently, you need to add a key property to them:
return (
<div>
{components.map((Comp, index) => {
const isActive = index === currentIndex;
return <Comp key={anAppropriateKeyValue} isActive={isActive} />;
})}
</div>
);
Don't just use index for key unless the order of the list never changes (but it's fine if the list is static, as it appears to be in your question). That might mean you need to change your array to an array of objects with keys and components. From the docs linked above:
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state. Check out Robin Pokorny’s article for an in-depth explanation on the negative impacts of using an index as a key. If you choose not to assign an explicit key to list items then React will default to using indexes as keys.
I suspect you're recreating the example array every time. That means that the functions you're creating in the array initializer are recreated each time, which means to React they're not the same component function as the previous render. Instead, make those functions stable. There are a couple of ways to do that, but for instance you can just directly use your LandingFirstStep and CameraOnboarding components in the map callback.
const components = [
{
Comp: LandingFirstStep,
props: {
// Any props for this component other than `isActive`...
onClick: progressToNextIndex
}
},
{
Comp: CameraOnboarding,
props: {
// Any props for this component other than `isActive`...
onNextClick: progressToNextIndex
}
},
];
then in the map:
{components.map(({Comp, props}, index) => {
const isActive = index === currentIndex;
return <Comp key={index} isActive={isActive} {...props} />;
})}
There are other ways to handle it, such as via useMemo or useCallback, but to me this is the simple way — and it gives you a place to put a meaningful key if you need one rather than using index.
Here's an example handling both of those things and showing when the components mount/unmount; as you can see, they no longer unmount/mount when the index changes:
const {useState, useEffect, useCallback} = React;
function LandingFirstStep({isActive, onClick}) {
useEffect(() => {
console.log(`LandingFirstStep mounted`);
return () => {
console.log(`LandingFirstStep unmounted`);
};
}, []);
return <div className={isActive ? "active" : ""} onClick={isActive && onClick}>LoadingFirstStep, isActive = {String(isActive)}</div>;
}
function CameraOnboarding({isActive, onNextClick}) {
useEffect(() => {
console.log(`CameraOnboarding mounted`);
return () => {
console.log(`CameraOnboarding unmounted`);
};
}, []);
return <div className={isActive ? "active" : ""} onClick={isActive && onNextClick}>CameraOnboarding, isActive = {String(isActive)}</div>;
}
const Example = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const progressToNextIndex = useCallback(() => {
setCurrentIndex(i => (i + 1) % components.length);
});
const components = [
{
Comp: LandingFirstStep,
props: {
onClick: progressToNextIndex
}
},
{
Comp: CameraOnboarding,
props: {
onNextClick: progressToNextIndex
}
},
];
return (
<div>
{components.map(({Comp, props}, index) => {
const isActive = index === currentIndex;
return <Comp key={index} isActive={isActive} {...props} />;
})}
</div>
);
};
ReactDOM.render(<Example/>, document.getElementById("root"));
.active {
cursor: pointer;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
When initializing widget component which loads various charts it works as it should, but when switching to another ListItem , componentDidMount do not load when switch to another item. I need to load it, because it fetch required data for it. But the thing is when I am switching between ListItem did not initialize componentDidMount
DashboardSidebar.jsx
class DashboardSidebar extends Component {
constructor(props) {
super(props);
this.state = {
tabLocation: this.props.tabLocation
};
this.onChange = this.onChange.bind(this);
}
onChange(event) {
this.setState({
tabLocation: event.target.value
});
}
render() {
const { classes } = this.props;
const { path } = this.props.match;
const { reports = [], sites = [] } = this.props;
let fromFilterString = "-"
let toFilterString = "-"
return (
<Drawer
className={classes.drawer}
variant="permanent"
classes={{
paper: classes.drawerPaper,
}}>
<div>
<List>
{reports.map((report) => (
<ListItem
onClick={this.onChange} button key={report.id}>
<ListItemIcon>
<DashboardIcon />
</ListItemIcon>
<ListItemText
disableTypography
primary={
<Typography type="body2">
<Link to={`${path}/${report.slug}`} style={{ color: "#000" }}>
{report.name}
</Link>
</Typography>
}
/>
</ListItem>
))}
</List>
</div>
<Divider light />
</Drawer>
)
}
}
This component seems run correct, but when clicking on other ListItem run componentDidUpdate which do not fetch required data for charts. Also I find out that when I changed in MainDashboard component key={i} to key={l.id} is started to hit componentDidMount, but then widget's do not load, but from Console I can see that it hit componentDidMount and fetch data which I console.log() .
MainDashboard.jsx
class MainDashboard extends Component {
componentDidUpdate(prevProps) {
if (this.props.match.params.report === prevProps.match.params.report) {
return true;
}
let widgets = {};
let data = {};
let layout = {};
fetch(...)
.then(response => response.json())
.then(data => {
...
this.setState({dashboard: data, isLoading: false, layouts: layout });
})
}
componentDidMount() {
this.setState({ mounted: true, isLoading: true });
let widgets = {};
let data = {};
let layout = {};
fetch(...
})
.then(response => response.json())
.then(data => {
...
this.setState({dashboard: data, isLoading: false, layouts: layout });
})
}
sortWidgets(widgets) {
...
return widgets;
}
generateDOM() {
return _.map(this.state.dashboard.widgets, function(l, i) {
....
return (
<div key={i}>
<ChartWidget
visualization={l.visualization}
name={l.visualization.name}
</div>
);
}.bind(this));
}
render() {
return (
...
);
}
}
You have three option:
1- Use another life cycle such as componentDidUpdate, fetch the new data if there is particular change in props or state
2- Use a new Key, if you use a new key on a component, it will trigger a componentDidMount, because the component is rendered again as a new entity
3- Use react.ref
I think you should read up on all three choices and choose one that will fit you the best.
So componentDidMount is actually being hit but nothing is happening in terms of data updates? in that case, your component loads/mounts first and whatever needs to happen doesn't trigger a re-render, I would look into using another lifecycle method to ensure that your data is updated.
I'm not sure if you're working on a legacy system but if upgrading to functional components is an option, I would recommend using the lifecycle method useEffect because it replaces many lifecycle methods like componentDidMount , componentDidUpdate and the unsafe componentWillUnmount and will make your life a whole lot easier and more efficient.
componentDidMount is only executed when a component is mounted.
A state update in DashboardSidebar would not cause BaseDashboard to be re-mounted so componentDidMount will not be re-executed for BaseDashboard.
Have you tried fetching the data in the onChange event handler (for switching to another ListItem) instead?
I'm making a dashboard that uses a common grid component. The grid has its own functionality separate and is used in other areas of the app. It needs to render a custom component within each grid item and has an active component that also renders a custom component, these custom components use functions from the Grids parent component, whatever is rendering it, below is how I do it current, but I'm pretty sure there is a better way of doing it.
Parent component that renders grid and passes down components and functions
import Grid from './common/grid'
class dashboard extends Component {
gridItemSpecificFunction() { console.log('success') }
activeFunction() { console.log('success again') }
render() {
return <Grid
CustomComponent={ CustomComponent }
ActiveComponent={ ActiveComponent }
activeFunctions={ {activeFunction} }
gridItemFunctions={ { gridItemSpecificFunction:this.gridItemSpecificFunction } }
/>
}
}
Grid that renders custom active and grid items based on data its passed
class Grid extends Component {
render() {
const {CustomComponent} = this.props
return (
<GridWrapper>
{ this.props.dynamicData.map( data => (
<GridItemWrapper>
<CustomComponent { ...data } functions={ this.props.gridItemFunctions } />
</GridItemWrapper>
) )
{ active && < ActiveComponent { ...activeData }
functions={ this.props.activeFunctions }/> }
</GridWrapper>
}
)
}
}
example of custom component that is using function passed through grid item
class CustomComponent extends Component {
render() {
const {gridItemSpecificFunction} = this.props.functions
return (
<div onClick={ gridItemSpecificFunction }>
{ this.props.text }
<div>
}
)
}
}
You actually doing great, of course there is another way to do this, probably is better just because become easier to modify, so you probably should use Context hook to get this done, so great packages based their functionalities in Context API Hook, so a great approach would be this
import React, { useContext, createContext, useMemo } from 'react';
const FunctionalityContext = createContext({}); // just leave it as empty state
// create a hook that return the context function
const useGridFunctions = () => {
const functions = useContext(FunctionalityContext);
return functions;
};
const CustomComponent = () => {
const functions = useGridFunctions();
return (
<div>
<button onClick={functions.gridItemSpecificFunction}>I am a custom component</button>
</div>
);
}
const ActiveComponent = () => {
const functions = useGridFunctions();
return (
<div>
<button onClick={functions.activeFunction}>I am a active component</button>
</div>
);
}
const ParentGrid = ({ functions }) => {
const functions = useMemo(() => functions, [functions]);
// the pass functions object to our context provider to get accesso through dom tree
return (
<FunctionalityContext.Provider value={functions}>
<Grid
CustomComponent={CustomComponent}
ActiveComponent={ActiveComponent}
/>
</FunctionalityContext.Provider>
);
}
As you can see you still keep your code almost the same, but you are adding a extra layer that will store the functionalities that will be used by components, so Context Api will help you to achieve this as you want.
Im making my first react project. Im new in JS, HTML, CSS and even web app programming.
What i want to do it is a Search input label. Now its look like this:
Like you can see i have some list of objects and text input.
I Have two components, my ProjectList.js with Search.js component...
class ProjectsList extends Component {
render() {
return (
<div>
<Search projects={this.props.projects} />
<ListGroup>
{this.props.projects.map(project => {
return <Project project={project} key={project.id} />;
})}
</ListGroup>
</div>
);
}
}
export default ProjectsList;
... and ProjectList.js displays Project.js:
How looks Search.js (its not ended component)
class Search extends Component {
state = {
query: ""
};
handleInputChange = () => {
this.setState({
query: this.search.value
});
};
render() {
return (
<form>
<input
ref={input => (this.search = input)}
onChange={this.handleInputChange}
/>
<p />
</form>
);
}
}
export default Search;
My project have name property. Could you tell me how to code Search.js component poperly, to change displaying projects dynamically based on input in text label? for example, return Project only, if text from input match (i want to search it dynamically, when i start typing m... it shows all projects started on m etc).
How to make that Search input properly? How to make it to be universal, for example to Search in another list of objects? And how to get input from Search back to Parent component?
For now, in react dev tools whatever i type there i get length: 0
Thanks for any advices!
EDIT:
If needed, my Project.js component:
class Project extends Component {
state = {
showDetails: false
};
constructor(props) {
super(props);
this.state = {
showDetails: false
};
}
toggleShowProjects = () => {
this.setState(prevState => ({
showDetails: !prevState.showDetails
}));
};
render() {
return (
<ButtonToolbar>
<ListGroupItem className="spread">
{this.props.project.name}
</ListGroupItem>
<Button onClick={this.toggleShowProjects} bsStyle="primary">
Details
</Button>
{this.state.showDetails && (
<ProjectDetails project={this.props.project} />
)}
</ButtonToolbar>
);
}
}
export default Project;
To create a "generic" search box, perhaps you could do something like the following:
class Search extends React.Component {
componentDidMount() {
const { projects, filterProject, onUpdateProjects } = this.props;
onUpdateProjects(projects);
}
handleInputChange = (event) => {
const query = event.currentTarget.value;
const { projects, filterProject, onUpdateProjects } = this.props;
const filteredProjects = projects.filter(project => !query || filterProject(query, project));
onUpdateProjects(filteredProjects);
};
render() {
return (
<form>
<input onChange={this.handleInputChange} />
</form>
);
}
}
This revised version of Search takes some additional props which allows it to be reused as required. In addition to the projects prop, you also pass filterProject and onUpdateProjects callbacks which are provided by calling code. The filterProject callback allows you to provide custom filtering logic for each <Search/> component rendered. The onUpdateProjects callback basically returns the "filtered list" of projects, suitable for rendering in the parent component (ie <ProjectList/>).
The only other significant change here is the addition of visibleProjects to the state of <ProjectList/> which tracks the visible (ie filtered) projects from the original list of projects passed to <ProjectList/>:
class Project extends React.Component {
render() {
return (
<div>{ this.props.project }</div>
);
}
}
class ProjectsList extends React.Component {
componentWillMount() {
this.setState({ visibleProjects : [] })
}
render() {
return (
<div>
<Search projects={this.props.projects} filterProject={ (query,project) => (project == query) } onUpdateProjects={ projects => this.setState({ visibleProjects : projects }) } />
<div>
{this.state.visibleProjects.map(project => {
return <Project project={project} key={project.id} />;
})}
</div>
</div>
);
}
}
class Search extends React.Component {
componentDidMount() {
const { projects, filterProject, onUpdateProjects } = this.props;
onUpdateProjects(projects);
}
handleInputChange = (event) => {
const query = event.currentTarget.value;
const { projects, filterProject, onUpdateProjects } = this.props;
const filteredProjects = projects.filter(project => !query || filterProject(query, project));
onUpdateProjects(filteredProjects);
};
render() {
return (
<form>
<input onChange={this.handleInputChange} />
</form>
);
}
}
ReactDOM.render(
<ProjectsList projects={[0,1,2,3]} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.0.0/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I will assumes both your Search and ProjectList component have a common parent that contains the list of your projects.
If so, you should pass a function into your Search component props, your Search component will then call this function when the user typed something in the search bar. This will help your parent element decide what your ProjectsLists needs to render :
handleInputChange = () => {
this.props.userSearchInput(this.search.value);
this.setState({
query: this.search.value
});
};
And now, here is what the parent element needs to include :
searchChanged = searchString => {
const filteredProjects = this.state.projects.filter(project => project.name.includes(searchString))
this.setState({ filteredProjects })
}
With this function, you will filter out the projects that includes the string the user typed in their names, you will then only need to put this array in your state and pass it to your ProjectsList component props
You can find the documentation of the String includes function here
You can now add this function to the props of your Search component when creating it :
<Search userSearchInput={searchChanged}/>
And pass the filtered array into your ProjectsList props :
<ProjectsList projects={this.state.filteredProjects}/>
Side note : Try to avoid using refs, the onCHnage function will send an "event" object to your function, containing everything about what the user typed :
handleInputChange = event => {
const { value } = event.target
this.props.userSearchInput(value);
this.setState({
query: value
});
};
You can now remove the ref from your code