React Component code organization best practise - reactjs

Is it ok to have all the functions inside of the App.js in React and then passing them to the other components through context:
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
listName: '',
existingListNames: [],
item: {
id: '',
toDo: ''
},
listItems: []
};
this.onItemChange = this.onItemChange.bind(this);
this.onItemAdd = this.onItemAdd.bind(this);
}
componentDidMount() {
axios.get('http://localhost:8080/v1/names')
.then(response => {
const listItems = response.data;
listItems.forEach(item => {
this.setState({
existingListNames: [...this.state.existingListNames, item.name]
})
})
})
}
onItemChange(event) {
this.setState({
item: {
id: uuid(),
[event.target.name]: event.target.value
}
});
}
onItemAdd(event) {
event.preventDefault();
const { item } = this.state;
this.setState({
listItems: [...this.state.listItems, item],
item: {
id: '',
toDo: ''
}
});
document.getElementById('formItem').reset();
}
render() {
const currentItem = this.onItemChange;
const addItem = this.onItemAdd;
const { listItems, existingListNames, listName } = this.state;
return (
<div className="main-container">
<Context.Provider
value={{
currentItem,
addItem,
removeItem,
listItems,
existingListNames,
listName
}}
>
<InputContainer />
</Context.Provider>
</div>
);
}
}
And then access them from the component:
import React, { useContext } from "react";
import Context from "../../../context/Context";
export default function ItemInput() {
const { currentItem, addItem } = useContext(Context);
return (
<form className="item" id="formItem">
<input
onChange={currentItem}
type="text"
className="newInput"
name="toDo"
placeholder="New Task"
autoComplete="off"
/>
<button onClick={addItem} className="checkButton">
<i className="fas fa-check fa-sm"></i>
</button>
</form>
);
}
Or is it better to separate them into different files and import straigth into components instead. What if App.js gets really big with lots of functions?

I'm going to assume you're looking for the "best practice" answer, since that's what you put in your title. That answer depends on your use case:
Propagating handlers to their respective children, should be done at the nearest parent to the child component. The only reason to use context would be if the parent-child relationship is not applicable to your Component hierarchy; i.e. one component to a random-component-nowhere-near-the-other.
Beyond one of those 2 scenarios, there's a lot of debate around the React community on using Context or Props for direct descendent children. To prepare yourself well for the eventual debate, feel free to read up on the pros & cons of each . If you do opt for context tho, there's some definite disadvantages.

Related

How do you filter array of objects in react using setState?

I have a simple array of objects but I can't seem to update state with the filtered values. If you console.log() the filteredData variable, the data is filtering correctly. However if I use the same variable inside setState() the filtered results aren't returning when console logging the people array. Does anyone know why this is happening? I'd also like to be able to re-render the list of filtered results. Do I need to use .map() inside the setState() method?
Thanks in advance.
import React from 'react';
import ReactDOM from 'react-dom';
import { v4 as uuidv4 } from 'uuid';
class App extends React.Component {
constructor(props) {
super(props);
this.handleSearch = this.handleSearch.bind(this);
this.state = {
people: [
{ id: uuidv4(), name: 'dave' },
{ id: uuidv4(), name: 'bryan' },
{ id: uuidv4(), name: 'abi' },
{ id: uuidv4(), name: 'chris' },
],
text: ''
}
}
handleSearch(e) {
const value = e.target.value.toLowerCase()
this.setState((prevState) => ({ text: value }));
}
render() {
const { people, text } = this.state;
const filteredData = people.filter((person) => {
return person.name.toLowerCase().includes(text.toLowerCase())
})
return (
<div>
<input type="text" name="searchPeople" placeholder="Search..." onChange={ this.handleSearch } />
<ul>
{
filteredData.map((person) => (<li key={ person.id }>{ person.name }</li>))
}
</ul>
</div>
);
}
}
const root = document.querySelector('#appRoot');
ReactDOM.render(<App />, root);
Edit both setState to retain the previous state unchanged properties this way:
this.setState({
...this.state,
people: filteredData,
});
this.setState({ ...this.state, filters: { text: value } });
Like #Cybershadow mentioned in the comment above, setState is asynchronous. And your log is being triggered before the value in this.state.people changes i.e. logged the previous state value. You can use a setState callback function to make use of the changed data state after a setState update is completed. And to use the setState callback, you need to pass the callback function as an second argument to the setState() method. In your case something like this:
this.setState(
{people: filteredData},
()=>console.log(this.state.people) //callback
);
More on React's setState() method.
#mjwals as setState is non concurrent the refreshed state will not be accessible quickly, so you can compose a callback work in the setState strategy inside the callback you will get the refreshed state, so from that point you can do other activity with the refreshed information. genuinely take a look at the code underneath
import React from 'react';
import "./styles.css";
import { v4 as uuidv4 } from 'uuid';
class App extends React.Component {
constructor(props) {
super(props);
this.handleSearch = this.handleSearch.bind(this);
this.state = {
people: [
{ id: uuidv4(), name: 'dave' },
{ id: uuidv4(), name: 'bryan' },
{ id: uuidv4(), name: 'abi' },
{ id: uuidv4(), name: 'chris' }
],
text: ''
}
}
handleSearch(e) {
const value = e.target.value.toLowerCase()
this.setState({ text: value }, () => {
const { people, text } = this.state;
const filteredData = people.filter((person) => {
return person.name.toLowerCase().includes(text.toLowerCase())
})
this.setState({ people: filteredData })
});
}
render() {
const { people } = this.state;
return (
<div>
<p>Please enter a input to search</p>
<input type="text" name="searchPeople" placeholder="Search..." onChange={this.handleSearch} />
<ul>
{people.map((person) => <li key={person.id}>{person.name}</li>)}
</ul>
</div>
);
}
}
export default App;

React trying to make a list of dynamic inputs

I have built this site
https://supsurvey.herokuapp.com/surveycreate/
now I am trying to move the fronted to React so I can learn React in the process.
with vanila js it was much easier to create elements dynamically.
I just did createElement and after that when I clicked "submit" button
I loop throw all the elements of Options and take each target.value input.
so I loop only 1 time in the end when I click Submit and that's it I have now a list of all the inputs.
in react every change in each input field calls the "OnChange" method and bubbling the e.targe.value to the parent and in the parent I have to copy the current array of the options and rewrite it every change in every field.
is there other way? because it seems crazy to work like that.
Options.jsx
```import React, { Component } from "react";
class Option extends Component {
constructor(props) {
super(props);
this.state = { inputValue: "", index: props.index };
}
myChangeHandler = event => {
this.setState({ inputValue: event.target.value });
this.props.onChange(this.state.index, event.target.value);
};
render() {
return (
<input
className="survey-answer-group"
type="text"
placeholder="Add Option..."
onChange={this.myChangeHandler}
/>
);
}
}
export default Option;
______________________________________________________________________________
Options.jsx````
```import React, { Component } from "react";
import Option from "./option";
class Options extends Component {
render() {
console.log(this.props);
return <div>{this.createOptions()}</div>;
}
createOptions = () => {
let options = [];
for (let index = 0; index < this.props.numOfOptions; index++) {
options.push(
<Option key={index} onChange={this.props.onChange} index={index} />
);
}
return options;
};
}
export default Options;```
______________________________________________________________________________
App.jsx
```import React from "react";
import OptionList from "./components/Options";
import AddButton from "./components/add-button";
import "./App.css";
class App extends React.Component {
state = {
numOfOptions: 2,
options: [{ id: 0, value: "" }, { id: 1, value: "" }]
};
handleChange = (index, value) => {
const options = [...this.state.options];
console.log("from App", value);
options[index].value = value;
this.setState({
options: options
});
console.log(this.state);
};
addOption = () => {
const options = [...this.state.options];
options.push({ id: this.state.numOfOptions + 1, value: "" });
this.setState({
numOfOptions: this.state.numOfOptions + 1,
options: options
});
};
submitButton = () => {};
render() {
return (
<div className="poll-create-grid">
<div id="poll-create-options">
<OptionList
onChange={this.handleChange}
numOfOptions={this.state.numOfOptions}
/>
</div>
<button
className="surveyCreate-main-btn-group"
onClick={this.addOption}
>
Add
</button>
<button
className="surveyCreate-main-btn-group"
onClick={this.submitButton}
>
Submit
</button>
</div>
);
}
}
export default App;
```
So firstly,
The issue is with the way your OptionList component is defined.
Would be nice to pass in the options from the state into the component rather than the number of options
<OptionList
onChange={this.handleChange}
options={this.state.options}
/>
The you basically just render the options in the OptionsList component (I'm assuming it's same as the Options one here
class Options extends Component {
...
render() {
return
(<div>{Array.isArray(this.props.options) &&
this.props.options.map((option) => <Option
key={option.id}
index={option.id}
onChange={this.props.onChange}
value={option.value}
/>)}
</div>);
}
...
}
You would want to use the value in the Option component as well.
this.props.onChange(this.state.index, event.target.value); No need using the state here to be honest
this.props.onChange(this.props.index, event.target.value); is fine

static getDerivedStateFromProps does't behave like componentwillreceiveprops?

Hi I'm trying to implement search in child component , the parent component will get data from server and pass that data to child component
as props, now child component has to implement search on that data, I have used componentwillreceiveprops which is depreciated how can I implement
this without using componentwillreceiveprops, below is my code
working example on fiddle
class Parent extends React.Component{
constructor(props) {
super(props);
this.state = {
data: []
}
}
componentDidMount() {
// mimic api call
const data = [
{ key: 'react'}, { key: 'redux'},
{ key: 'javascript' }, { key: 'Ruby'} ,{key: 'angular'}
]
setTimeout(this.setState({data}), 3000);
}
render() {
return (
<React.Fragment>
<ChildComponent data = {this.state.data}/>
</React.Fragment>
)
}
}
class ChildComponent extends React.Component{
constructor(props) {
super(props);
this.state = {
data: []
}
}
componentwillreceiveprops(nextProps){
this.setState({data: nextProps.data})
}
search(e){
console.log('props,', e.target.value)
let searchedData = this.props.data.filter(el => {
return el.key.startsWith(e.target.value)
})
this.setState({data: searchedData})
};
render(){
return(
<div>
search for (react, redux, angular, ruby)
<br/> <br/> <br/>
<input type = 'text' onChange={this.search.bind(this)}/>
{this.state.data.map(d => {
return (<div key={d.key}>{d.key}</div>)
})}
</div>
)
}
}
getDerivedStateFromProps is not a direct replacement for componentWillReceiveProps. Its just meant to update state in response to any update and unlike componentWillReceiveProps, getDerivedStateFromProps is triggered on every update either from child or from parent so you cannot simply update state without any conditional check. In order to update state if the props changed, you need to store the previous props in the state of child too or update the key of child so that it triggers a re-render
There are two possible approaches to this. Below is an example of first approach with getDerivedStateFromProps
import React from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import "./styles.css";
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
componentDidMount() {
// mimic api call
const data = [
{ key: "react" },
{ key: "redux" },
{ key: "javascript" },
{ key: "Ruby" },
{ key: "angular" }
];
setTimeout(() => {
this.setState({ data });
setTimeout(() => {
this.setState(prev => ({ data: [...prev.data, { key: "Golang" }] }));
}, 3000);
}, 3000);
}
render() {
return (
<React.Fragment>
<ChildComponent data={this.state.data} />
</React.Fragment>
);
}
}
class ChildComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
static getDerivedStateFromProps(props, state) {
if (!_.isEqual(props.data, state.prevData)) {
return {
data: props.data,
prevData: state.data
};
} else {
return {
prevData: props.data
};
}
}
search(e) {
console.log("props,", e.target.value);
let searchedData = this.props.data.filter(el => {
return el.key.startsWith(e.target.value);
});
this.setState({ data: searchedData });
}
render() {
return (
<div>
search for (react, redux, angular, ruby)
<br /> <br /> <br />
<input type="text" onChange={this.search.bind(this)} />
{this.state.data.map(d => {
return <div key={d.key}>{d.key}</div>;
})}
</div>
);
}
}
function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Parent />, rootElement);
Working DEMO
Second approach involves changing the key of the child component instead of implementing getDerivedStateFromProps
import React from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import "./styles.css";
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
keyData: 0
};
}
componentDidMount() {
// mimic api call
const data = [
{ key: "react" },
{ key: "redux" },
{ key: "javascript" },
{ key: "Ruby" },
{ key: "angular" }
];
setTimeout(() => {
this.setState(prev => ({ data, keyData: (prev.keyData + 1) % 10 }));
setTimeout(() => {
this.setState(prev => ({
data: [...prev.data, { key: "Golang" }],
keyData: (prev.keyData + 1) % 10
}));
}, 3000);
}, 3000);
}
render() {
return (
<React.Fragment>
<ChildComponent data={this.state.data} key={this.state.keyData} />
</React.Fragment>
);
}
}
class ChildComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: props.data
};
}
search(e) {
console.log("props,", e.target.value);
let searchedData = this.props.data.filter(el => {
return el.key.startsWith(e.target.value);
});
this.setState({ data: searchedData });
}
render() {
return (
<div>
search for (react, redux, angular, ruby)
<br /> <br /> <br />
<input type="text" onChange={this.search.bind(this)} />
{this.state.data.map(d => {
return <div key={d.key}>{d.key}</div>;
})}
</div>
);
}
}
function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Parent />, rootElement);
Working DEMO
You can go ahead with the second method when you know that you will have quite a few updates in the child component whereas update from parent will be less frequent and the props that you have to compare getDerivedStateFromProps is nested . In such as case implementing getDerivedStateFromProps will be less performant than updating the key since you will need to perform expensive computation on each render.
To implement your componentWillReceiveProps() behavior using the new getDerivedStateFromProps() method, you can replace your current componentwillreceiveprops() hook with this:
static getDerivedStateFromProps(nextProps, state){
/* Return the new state object that should result from nextProps */
return { data : nextProps.data }
}
The getDerivedStateFromProps() will be called before your component is rendered and, if a non-null value is returned, then that return value will become the state of the component.
In your case, the state of the <ChildComponent> component has only one data field which is populated directly from props, so returning { data : nextProps.data } would be sufficent to update the data state field to match the incoming data prop.
The general idea is that you can use this method to update a component's state based on changing/incoming props.
See this documentation for more information on getDerivedStateFromProps() - hope that helps!
Update
Also on another note, it seems the way <Parent> is updating state via the setTimeout method is incorrect. You should update it as follows:
// Incorrect: setTimeout(this.setState({data}), 3000);
setTimeout(() => this.setState({data}), 3000);

TODO project is not woking properly

Components ->
Box
Todolist
Add
AddModal
Main component App
But it is not working that is when I add a new task. It does not get added properly.
I think I cannot use this.setstate twice in a function.
Hope I am correct
Here is given the main component.
App.js :
import React, { Component } from 'react';
import './App.css';
import Box from './Components/Box';
import Add from './Components/Add';
import Todolist from './Components/Todolist';
class App extends Component {
constructor(props) {
super(props);
this.state = {
lists: '',
inputValue: '',
itemArray: []
}
}
onAddTask = () => {
this.setState ({
lists: this.state.inputValue
});
const item = this.state.itemArray;
const title = this.state.lists;
item.push({ title })
this.setState(prevState => ({
itemArray: [...prevState.lists, title]
}))
}
updateInputValue = (event) => {
this.setState({
inputValue: event.target.value
});
}
render() {
let length = this.state.itemArray.length;
return (
<div className="App">
<Box createTodo = {
<div>
{this.state.itemArray.map((itemArr) => {
return (
<div className="box">
<Todolist tasks = {itemArr} />
</div>
)
})}
</div>
}>
</Box>
<Add addTask = {this.onAddTask} inputValues = {this.updateInputValue} inputV = {this.state.inputValue} />
</div>
);
}
}
export default App;
Your addTasks function is not correct, you are mixing up things here.
In your inputValue you save the current value from the input field right? So if you write the following
this.setState({
lists: this.state.inputValue
});
you set your todo list to this single value. And your todo list is not an array anymore.
Secondly, state is imutable. So if you write the following
this.state.itemArray.push({ title });
the state will not be updated. What you actually want is the following:
onAddTask = () => {
this.setState({
itemArray: [...this.state.itemArray, this.state.inputValue]
})
}
And I'm not sure what the lists property on the state is for. You don't use it anywhere besides in your onAddTask function. So I guess you can remove it.

React - Splitting large comment component into multiple components

I have a large Comment component which works great but is fairly lengthy. I have recently added a report button to the UI which toggles a reported state which should then change the output of the comment (removing everything and displaying a reported message). Trying to write the if statement in the return method of the component made me realise that I should be splitting this component up (not to mention that I have found myself copy/pasting a lot of code between this Comment component and the very similar Reply component).
The comment has 3 main 'views' - the default view, the reported view and the 'my comment' view.
Whenever I have tried to split up components in the past I have found myself getting bogged down in the passing of multiple props to each component. I'm not sure whether I'm doing it wrong or whether it is just something I need to get used to. Any tips on the best way of splitting this component up would be appreciated.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { replyToCommentService, deleteCommentService, reportCommentService } from '../../../services/CommentService';
import { likeService, removeLikeService } from '../../../services/LikeService';
import Reply from './Reply';
import Avatar from '../Avatars/Avatar';
import IconWithText from '../Icons/IconWithText';
import CommentForm from './CommentForm';
import Dropdown from '../Dropdowns/Dropdown';
import DropdownSection from '../Dropdowns/DropdownSection';
export default class Comment extends Component {
constructor(props) {
super(props);
this.state = {
replies: this.props.replies,
showReply: false,
reply: '',
replyBtnDisabled: true,
liked: this.props.liked,
numberOfLikes: this.props.likes.length,
moreActionsActive: false,
reported: this.props.reported,
};
}
handleInput = (reply) => {
this.setState({ reply }, () => {
this.fieldComplete();
});
}
fieldComplete = () => {
if (this.state.reply.length) {
this.setState({ replyBtnDisabled: false });
} else {
this.setState({ replyBtnDisabled: true });
}
}
toggleReply = () => {
this.setState({ showReply: !this.state.showReply }, () => {
if (this.state.showReply === true) {
this.replyInput.focus();
}
});
}
postReply = () => {
const data = { comment_id: this.props.id, comment_content: this.state.reply };
replyToCommentService(data, this.postReplySuccess, this.error);
}
postReplySuccess = (res) => {
this.setState({ replies: this.state.replies.concat(res.data) });
this.toggleReply();
this.handleInput('');
}
error = (res) => {
console.log(res);
}
toggleLike = (e) => {
e.preventDefault();
const data = { model_id: this.props.id, model_type: 'comment' };
if (this.state.liked) {
removeLikeService(this.props.id, 'comment', this.removeLikeSuccess, this.error);
} else {
likeService(data, this.likeSuccess, this.error);
}
}
likeSuccess = () => {
this.toggleLikeState();
this.setState({ numberOfLikes: this.state.numberOfLikes += 1 });
}
removeLikeSuccess = () => {
this.toggleLikeState();
this.setState({ numberOfLikes: this.state.numberOfLikes -= 1 });
}
toggleLikeState = () => {
this.setState({ liked: !this.state.liked });
}
moreActionsClick = () => {
this.setState({ moreActionsActive: !this.state.moreActionsActive });
}
deleteReply = (replyId) => {
this.setState({ deletedReplyId: replyId });
deleteCommentService(replyId, this.deleteReplySuccess, this.error);
}
deleteReplySuccess = () => {
this.setState(prevState => ({ replies: prevState.replies.filter(reply => reply.id !== this.state.deletedReplyId) }));
}
ifEnterPressed = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.postReply();
}
}
reportComment = () => {
const data = { model_id: this.props.id, model_type: 'comment' };
reportCommentService(data, this.reportCommentSuccess, this.error);
}
reportCommentSuccess = (res) => {
console.log(res);
}
render() {
let repliesList;
if (this.state.replies.length) {
repliesList = (this.state.replies.map((reply) => {
const { id, owner_id, content, owner_image_url, owner_full_name, ago, likes, liked } = reply;
return (
<Reply
key={id}
id={id}
authorId={owner_id}
title={content}
image={owner_image_url}
authorName={owner_full_name}
timeSinceComment={ago}
likes={likes}
liked={liked}
newComment={this.newCommentId}
deleteReply={this.deleteReply}
/>
);
}));
}
const commentClass = classNames('comment-container', {
'my-comment': this.props.myComment,
'comment-reported': this.state.reported,
});
let likeBtnText;
const numberOfLikes = this.state.numberOfLikes;
if (numberOfLikes > 0) {
likeBtnText = `${numberOfLikes} Likes`;
if (numberOfLikes === 1) {
likeBtnText = `${numberOfLikes} Like`;
}
} else {
likeBtnText = 'Like';
}
const likeBtnClass = classNames('like-btn', 'faux-btn', 'grey-link', 'h5', {
liked: this.state.liked,
});
let likeIconFill;
if (this.state.liked) {
likeIconFill = 'green';
} else {
likeIconFill = 'grey';
}
return (
<li className={commentClass}>
<div className="comment">
<Avatar image={this.props.image} />
<div className="body">
<div className="header">
{this.props.authorName}
<span className="h5 text-grey">{this.props.timeSinceComment}</span>
<Dropdown
size="S"
position="right"
onClick={this.moreActionsClick}
active={this.state.moreActionsActive}
handleClickOutside={this.moreActionsClick}
disableOnClickOutside={!this.state.moreActionsActive}
>
<DropdownSection>
{this.props.myComment &&
<button className="faux-btn dropdown-link" onClick={() => this.props.deleteComment(this.props.id)}>Delete comment</button>
}
</DropdownSection>
<DropdownSection>
<button className="faux-btn dropdown-link" onClick={() => this.reportComment(this.props.id)}>Report as inappropriate</button>
</DropdownSection>
</Dropdown>
</div>
<div className="comment-text"><p>{this.props.title}</p></div>
<div className="actions">
<button onClick={this.toggleLike} className={likeBtnClass}>
<IconWithText text={likeBtnText} iconName="thumb-up" iconSize="S" iconFill={likeIconFill} />
</button>
<button onClick={this.toggleReply} className="reply-btn faux-btn grey-link h5">
<IconWithText text="Reply" iconName="reply" iconSize="S" iconFill="grey" />
</button>
</div>
</div>
</div>
{this.state.replies.length > 0 &&
<div className="replies-container">
<ul className="replies-list faux-list no-margin-list">
{repliesList}
</ul>
</div>
}
{this.state.showReply &&
<div className="reply-to-comment-form">
<CommentForm
commentContent={this.handleInput}
postComment={(e) => { e.preventDefault(); this.postReply(); }}
formDisabled={this.state.replyBtnDisabled}
placeholder="Write a reply... press enter to submit"
btnText="Reply"
inputRef={(input) => { this.replyInput = input; }}
handleKeyPress={this.ifEnterPressed}
/>
</div>
}
</li>
);
}
}
Comment.propTypes = {
id: PropTypes.number,
authorId: PropTypes.number,
title: PropTypes.string,
image: PropTypes.string,
authorName: PropTypes.string,
timeSinceComment: PropTypes.string,
likes: PropTypes.array,
liked: PropTypes.bool,
replies: PropTypes.array,
myComment: PropTypes.bool,
deleteComment: PropTypes.func,
newCommentId: PropTypes.number,
reported: PropTypes.bool,
};
Well the general problem is where your state lives.
Currently you have your state in the component (and/or services), that makes splitting up the component somewhat tricky and not feeling so "natural".
The reason is that each natural sub component (for example a replies list, or a reply itself) requires pieces of that state and perhaps also needs to modify that state, that's then being done by the "property passing" and it can be tedious. Passing pieces of the state down to sub components and/or passing event callbacks down such as "upDateState" "showThis" "showThat".
This is sometimes what you want, you can then create a stateless components that only renders ui, a list of answers for example. If this is what you want then yes, you just have to get used to passing in props from parents.
The other answer to continue growing you application is modeling it by its state and the only way to do that (properly) is to abstract the application state away from the component. Creating a state that does not live inside of a component, a state that every component can access.
You might have guess what my suggestion is by now, have a look at Redux (or similar state management lib.) and you can easily cut out pieces (components) and attach them to the Redux global state and action. Once you get used to "never" keep application state in your components you wont go back. :)
PS!
This is perhaps not an answer but its to long for a comment. Just wanted to share my thoughts.

Resources