I have a class called: QuestionList, which creates Questions children, along with (nested) Alternatives:
QuestionList render:
<Question wording="wording...">
<Alternative letter="a" text="bla ..." />
<Alternative letter="b" text="ble ..." />
<Alternative letter="c" text="bli ..." />
<Alternative letter="d" text="blo ..." />
</Question>
Who is "alternatives" parent? Question (because it is nested) or QuestionList (because it created)?
How can pass a Question event handler to Alternative?
If I use
<Alternative onClick={this.handleClick} (...) />
It will pass QuestionList's handler (and not Question's handler - the desired behavior).
QuestionList
import React, { Component } from 'react';
import InfiniteScroll from 'react-infinite-scroller';
import Loader from 'react-loaders';
import Question from './Question';
import Alternative from './Alternative';
export default class QuestionList extends Component {
constructor(props) {
super(props);
this.state = {
questions: []
};
}
loadItems(page) {
let questions = this.state.questions;
axios.get('https://jsonplaceholder.typicode.com/photos?_start='+ page * 5 +'&_limit=5')
.then(response => {
console.log(response.data);
response.data.map(p => {
questions.push(p);
});
this.setState({questions});
});
}
handleClick() {
alert("QuestionList");
}
render() {
let items = [];
const loader = <Loader type="ball-scale-multiple" />;
this.state.questions.map((p, i) => {
items.push(
<Question
title={p.title}
key={i}
id={p.id}
>
<Alternative onClick={this.props.handleClick} key={1} text={ p.title } letter="a" />
<Alternative onClick={this.props.handleClick} key={2} text={ p.title } letter="b" />
<Alternative onClick={this.props.handleClick} key={3} text={ p.title } letter="c" />
<Alternative onClick={this.props.handleClick} key={4} text={ p.title } letter="d" />
<Alternative onClick={this.props.handleClick} key={5} text={ p.title } letter="e" />
</Question>
)
});
return (
<InfiniteScroll
key={1}
pageStart={0}
loadMore={this.loadItems.bind(this)}
hasMore={true}
loader={loader}
>
<div className="justify-content-center" id="react-app-questions-list">
{items}
</div>
</InfiniteScroll>
);
}
}
Question
import React, { Component } from 'react';
export default class Question extends Component {
constructor(props) {
super(props);
this.state = {
answer_class: "unanswered"
};
}
handleClick(isCorrect, e) {
// alert(this.props.id + ": " + isCorrect);
alert("Question");
}
render() {
return (
<div className={"list-group list-group-bordered mb-3 " + this.state.answer_class}>
<div className="list-group-item">
<div className="list-group-item-body">
<h4 className="list-group-item-title">
{ this.props.title }
</h4>
</div>
</div>
{ this.props.children }
</div>
);
}
}
Alternative
import React, { Component } from 'react';
class Alternative extends Component {
render() {
return (
<a className="list-group-item list-group-item-action react-app-alternative">
<div className="list-group-item-figure">
<div className="tile tile-circle bg-primary">{ this.props.letter }</div>
</div>
<div className="list-group-item-body"> { this.props.text }</div>
</a>
);
}
}
export default Alternative;
Who is Alternatives parent? Question (because it is nested) or QuestionList (because it created)?
Alternative parent is Question. If you check Question.props.children array (remember that Question is just an object), you will see Alternative types there.
function Question({ children }) {
console.log(children); // children[0].type === Alternative
return children;
}
Read more about React elements as objects here.
How can pass a Question event handler to Alternative?
You can inject props to Question children, for example:
function Question({ children }) {
console.log(children);
const injectChildren = React.Children.map(children, child =>
React.cloneElement(child, { letter: `${child.props.letter}-injected` })
);
return injectChildren;
}
For this you need to read about React Top-Level API and refer to React.Children API and cloneElement().
Check out the example:
Handlers are intended to work on context ... where state is managed .. then in <QuestionList/>. Prepare specific, parametrized handlers and use them to update common state.
Chaining 'desired' (more granular or more specific) handlers to pass values through the structure can't be practical. It won't be efficient, either.
Take a look at data/state flow in 'Formik` project - form, validations, fields. It can be a good source of inspiration for this problem.
<Question/> and <Alternative/> should be stateless, functional components - you don't need them to be statefull. KISS, YAGNI...
Related
Here's my structure :
Main.js (Parent)
MainContainer.js
|
|_ Article.js
|
|__ Comments.js
Now i want to set click handler on comment component (recursive component) and dispatch an action.
here's my code on comment.js
class Comment extends Component {
deleteComment = (id) => {
this.props.handleDelete(id)
}
render() {
var comment = this.props.comment
return (
<div className={styles.commentsWrapper}>
<ul>
<li>
<div className={styles.commentsName}>
<a onClick={() => this.deleteComment(comment.id)} className={styles.commentsNameRight}>
</a>
</div>
<p>{comment.body}</p>
{comment.children.length > 0 && comment.children.map(function(child) {
return <Comment comment={child} key={child.id}/>
})}
</li>
</ul>
</div>
);
}
}
export default Comment;
and Article.js :
class Article extends Component {
handleDeleteComment = (id) => {
this.props.deleteComment(id)
}
render() {
return (
<article className={styles.articleItem}>
{this.props.comments.map(item =>
<Comment handleDelete={this.handleDeleteComment} comment={item} key={item.id}/>)}
</article>
);
}
}
export default Article;
And the Main.js
class Main extends Component {
deleteComment = (id) => {
this.props.deleteCommentRequest(id)
}
render() {
return (
<div className="">
<Header />
<section className="container">
<div>
{
!this.props.articles.loading && this.props.articles.articles? (
<div>
{this.props.articles.articles.map(item =>
<Article
bodytext={item.selftext}
key={item.id}
comments={item.finalComments}
deleteComment={this.deleteComment}
/>)}
</div>
) : (
<div className={styles.loading}> <Spin /> </div>
)
}
</div>
</section>
</div>
);
}
}
export default Main;
so what i did here is: pass deleteComment as props from main to article and pass again handleDelete from article to comment.
not sure if it's a good way of doing this ?
Thanks in advance
Nothing wrong with this pattern for 2 - 3 depth of components, as that is how data should flow from children to ancestors. But if your application is getting heavier with several layers, consider a different state management such as redux where a global state is maintained and any component can subscribe to it and dispatch actions. More on that here.
Alternatively you can also achieve the same with React Hooks with useContext where you can set the context and any child component can subscribe to it. Example:
const MyContext = React.createContext();
export default function App({ children }) {
const [items, setItems] = React.useState([]);
return (
<MyContext.Provider value={{ items, setItems }}>
{children}
</MyContext.Provider>
);
}
export { MyContext };
Now in any child at any level of depth as long as it is within App component's children, you can do this:
import {MyContext} from './filename';
function TodoItem() {
const { items, setItems } = React.useContext(MyContext);
return (
<div onClick={() => setItems(1)}>
</div>
);
}
you can use context API to have the props in the wrapper and easily accessible from child component.
there is a great tutorial from wesbos on youtube
class App extends Component {
render() {
return (
<MyProvider>
<div>
<p>I am the app</p>
<Family />
</div>
</MyProvider>
);
}
}
class MyProvider extends Component {
state = {
name: 'Wes',
age: 100,
cool: true
}
render() {
return (
<MyContext.Provider value={{
state: this.state,
growAYearOlder: () => this.setState({
age: this.state.age + 1
})
}}>
{this.props.children}
</MyContext.Provider>
)
}
}
I have such a component:
import React, { Component } from 'react';
export class TopicsList extends Component {
constructor(props) {
super(props);
this.state = {
topics: [...],
};
this.references = [];
}
getOrCreateRef(id) {
if (!this.references.hasOwnProperty(id)) {
this.references[id] = React.createRef();
}
return this.references[id];
}
render() {
const {
topics
} = this.state;
return (
<div>
<ul>
{topics.map((topic) => (
<TopicItem
key={topic.id}
topic={topic}
ref={this.getOrCreateRef(topic.id)}
/>
)
)}
</ul>
</div>
)
}
}
const TopicItem = React.forwardRef((props, ref) => {
return (
<li
>
<p>{props.name}</p>
<i
className="fal fa-plus"
/>
</li>
);
});
I wrote test to test how much li items will be rendered:
test('should render 3 li items', () => {
console.log(wrapper.debug())
expect(wrapper.find('TopicItem').length).toBe(3);
});
but my test failed because in jest they recognized like:
<ul>
<ForwardRef topic={{...}} />
<ForwardRef topic={{...}} />
<ForwardRef topic={{...}} />
</ul>
How can I test components that are returned with React.forwardRef?
I cannot find appropriate solutions on the internet or here.
It is a bit late, but assigning the displayName property to the wrapped component can help. Enzyme respects displayName and uses it when creating snapshots (rendering it instead of ForwardRef in this case), and find also works with display names.
So I'm trying to break the component on my App.js into a smaller component, that being my Sidebar.js. I took a small section of the code and put it in its own Sidebar.js file but no matter what I've tried, I cant call my function getNotesRows() from App.js without it being unable to find it or this.states.notes being undefined.
I just want it to send the code back and forth. This is a demo app, so I know it's not the most practical.
import React, { Component } from "react";
import classNames from "classnames";
import logo from "./logo.svg";
import checkMark from "./check-mark.svg";
import "./App.css";
import Sidebar from "./components/Sidebar.js";
class App extends Component {
constructor(props) {
super(props);
this.state = {
notes: [],
currentNoteIndex: 0
};
this.markAsRead = this.markAsRead.bind(this);
this.selectNote = this.selectNote.bind(this);
console.log("Test started 2.25.19 19:23");
}
componentWillMount() {
fetch('/notes')
.then(response => response.json())
.then(
notes => {
this.setState({
notes: notes,
currentNoteIndex: 0
})
}
)
.catch(
error => {
console.log('Ooops!');
console.log(error);
}
);
}
markAsRead() {
this.setState(currentState => {
let marked = {
...currentState.notes[currentState.currentNoteIndex],
read: true
};
let notes = [...currentState.notes];
notes[currentState.currentNoteIndex] = marked;
return { ...currentState, notes };
});
}
selectNote(e) {
this.setState({ currentNoteIndex: parseInt(e.currentTarget.id, 10) });
}
getTotalUnread() {
let unreadArray = this.state.notes.filter(note => {
return note.read === false;
})
return unreadArray.length;
}
getNotesRows() {
return this.props.notes.map(note => (
<div
key={note.subject}
className={classNames("NotesSidebarItem", {
selected:
this.props.notes.indexOf(note) === this.props.currentNoteIndex
})}
onClick={this.selectNote}
id={this.props.notes.indexOf(note)}
>
<h4 className="NotesSidebarItem-title">{note.subject}</h4>
{note.read && <img alt="Check Mark" src={checkMark} />}
</div>
));
}
// TODO this component should be broken into separate components.
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Notes Viewer Test App</h1>
<div>
Unread:
<span className="App-title-unread-count">
{this.getTotalUnread()}
</span>
</div>
</header>
<div className="Container">
<Sidebar />
<section className="NoteDetails">
{this.state.notes.length > 0 && (
<h3 className="NoteDetails-title">
{this.state.notes[this.state.currentNoteIndex].subject}
</h3>
)}
{this.state.notes.length > 0 && (
<p className="NoteDetails-subject">
{this.state.notes[this.state.currentNoteIndex].body}
</p>
)}
{this.state.notes.length > 0 && (
<button onClick={this.markAsRead}>Mark as read</button>
)}
{this.state.notes.length <= 0 && (
<p>
No Notes!
</p>
)}
</section>
</div>
</div>
);
}
}
export default App;
Above is my App.js
and below is the Sidebar.js that I'm trying to create
import React, { Component } from "react";
import "../App.css";
import App from "../App.js";
class Sidebar extends React.Component{
constructor(props) {
super(props);
}
render(){
return (
<section className="NotesSidebar">
<h2 className="NotesSidebar-title">Available Notes:</h2>
<div className="NotesSidebar-list">{App.getNotesRows()}</div>
</section>
)}}
export default Sidebar;
You cannot access a method like that. You need to pass the method as a prop and use it in the child.
<Sidebar getNotesRows={this.getNotesRows} />
and in Sidebar use
<div className="NotesSidebar-list">{this.props.getNotesRows()}</div>
In your sidebar, you're trying to call getNotesRows() from App, but Sidebar doesn't need access to app (you shouldn't have to import App in Sidebar.js). Instead, you should pass the function from App to your Sidebar component, and reference it from Sidebar's props.
In App.js, you'll need to bind getNotesRows and pass it to sidebar.:
<Sidebar getNotesRows={ this.getNotesRows } />
Then in Sidebar.js, you'll need to reference getNotesRows in your render method:
render() {
const notes = this.props.getNotesRows();
return (
<section className="NotesSidebar">
<h2 className="NotesSidebar-title">Available Notes:</h2>
<div className="NotesSidebar-list">{ notes }</div>
</section>
);
}
It seems like the problem here is that you are trying to use a class function as a static property, to put it simply, you have not initialized the App class when you import it into your sidebar(?), thus no static function was found on your App class so you can call App.getNotesRows() maybe you should re-think your components and separate them in container-components using a Composition Based Programming approach instead of OO approach.
In particular, I'm curious about the way to pass information along. In another thread, I had child components and the methods of passing certain props explained to me, as well as the dangers of <MyComponent children={...} />, and I'm curious which would be better: storing and working with the tabs as a mapped array of objects as in the example, or following the
<TabList> <Tab /><Tab /> </TabList> style. Is <MyComponent children={...} /> the same as <TabList tabs={this.state.tabs} />? I assume so, but apparently children as a prop is special case?
const tabs = [
{
id: 0,
label: "Archery",
content: "Lorem Ipsum 1"
},
{
id: 1,
label: "Baseball",
content: "Lorem Ipsum 2"
}
];
function TabContent(props) {
return (
<div className="tabContent">
{props.content}
</div>
);
}
class Tab extends React.Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick(el) {
this.props.handleClick(el.target)
}
render() {
let active = (this.props.id === this.props.activeTab) ? "active" : ""
return (
<li id={this.props.id} onClick={this.onClick} className={active}>
{this.props.label}
</li>
);
}
}
class TabList extends React.Component {
constructor(props) {
super(props);
}
componentDidUpdate() {
$(this.refs.tabList).animate({scrollLeft: this.props.scrollPosition}, 400)
}
render() {
let tabList = this.props.tabs.map((tab) => {
return (
<Tab
key={tab.id}
id={tab.id}
activeTab={this.props.activeTab}
label={tab.label}
handleClick={this.props.handleClick}
/>
);
});
return (
<ul className="tabList" ref="tabList">
{tabList}
</ul>
);
}
}
class TabScroller extends React.Component {
render() {
return (
<div className="tabScroller">
<div className="NavList">
<TabNav handleClick={this.handleNavClick} />
<TabList
tabs={this.state.tabs}
activeTab={this.state.activeTab}
scrollPosition={this.state.scrollPosition}
handleClick={this.handleTabClick}
/>
</div>
<TabContent content={this.state.tabs[this.state.activeTab].content} />
</div>
);
}
}
// ========================================
ReactDOM.render(
<TabScroller />,
document.getElementById('root')
);
The usage of <TabList> <Tab /><Tab /> </TabList> is just a syntax sugar that JSX has brought. Internally, it's the same as <TabList children={[<Tab />, <Tab />]} />.
As it turns out, anything within <TabList></TabList> will be passed to the TabList component as props.children which includes all functions and props that might be added to child Components. This, among other reasons, is why it's more often best to pass the children as child Components rather than props and avoid having to unnecessarily duplicate props.
I have no single resource to officially confirm this, just experience gained through playing around in React.js, so if anyone can comment or DM me a link, I'll add it to the answer.
Basically new to React, I'm a bit confused on how to properly pass states between components. I found a similar question already React – the right way to pass form element state to sibling/parent elements?
but I wonder if you can give me a specific answer for the code below.
Currently the structure of the app includes:
parent component - App
2 childs: SearchBar and RecipesList
The goal is to make an async search on my Meteor collection and display only the recipes that match the search term.
Right now, I'm just showing all the recipes in my Meteor collection.
I've created a stateful component named SearchBar which holds the input value as this.state.term. The idea is to pass the state to RecipesList but I'm not sure if it's the right thing to do. Alternatively I'd let App deal with the state and passing it to the childs. I believe this is a very common scenario, how do you do it?
App
class App extends Component {
render( ) {
return (
<div>
<Navbar/>
<SearchBar/>
<RecipesList/>
</div>
);
}
}
SearchBar
export default class SearchBar extends Component {
constructor( props ) {
super( props );
this.state = {
term: ''
};
}
onInputChange( term ) {
this.setState({ term });
}
render( ) {
return (
<div className=" container-fluid search-bar">
<input value={this.state.term} onChange={event => this.onInputChange(event.target.value.substr( 0, 50 ))}/>
Value: {this.state.term}
</div>
);
}
}
RecipesList
const PER_CLICK = 5;
class RecipesList extends Component {
componentWillMount( ) {
this.click = 1;
}
handleButtonClick( ) {
Meteor.subscribe('recipes', PER_CLICK * ( this.click + 1 ));
this.click++;
}
renderList( ) {
return this.props.recipes.map(recipe => {
return (
<div key={recipe._id} className="thumbnail">
<img src={recipe.image} alt="recipes snapshot"/>
<div className="caption">
<h2 className="text-center">{recipe.recipeName}</h2>
</div>
</div>
);
});
}
render( ) {
return (
<ul className="list-group">
{this.renderList( )}
<div className="container-fluid">
<button onClick={this.handleButtonClick.bind( this )} className="btn btn-default">More</button>
</div>
</ul>
);
}
}
// Create Container and subscribe to `recipes` collection
export default createContainer( ( ) => {
Meteor.subscribe( 'recipes', PER_CLICK );
return {recipes: Recipes.find({ }).fetch( )};
}, RecipesList );
App
class App extends Component {
constructor(props, ctx){
super(props, ctx)
this.state = {
searchQuery: ''
}
this.searchInputChange = this.searchInputChange.bind(this)
}
searchInputChange(event) {
this.setState({
searchQuery: event.target.value.substr( 0, 50 )
})
}
render( ) {
const { searchQuery } = this.state
return (
<div>
<Navbar/>
<SearchBar onChange={this.searchInputChange} value={searchQuery}/>
<RecipesList searchQuery={searchQuery}/>
</div>
)
}
}
The App component takes care of the state and this is then passed down to the children as props the seach term is available to RecipesList through props.searchQuery.
The searchInputChange handler is passed down to the SearchBar as props.
SearchBar
export default const SearchBar = ({value, onChange}) => (
<div className=" container-fluid search-bar">
<input value={value} onChange={onChange}/>
Value: {value}
</div>
)
Since the SearchBar delegated state to the parent component, we can use a stateless react component as we only need information from the props to render it.
In general it is always best to have a logical or stateful or controller component take care of state and the logic, this component then passes down state and methods to presentational or view components which take care of what the user sees and interacts with.
Define the state term up in to the App component.
Also write the handleInput function and pass it to the SearchBar component as porps
handleInput(val) {
this.setState({
term: val,
});
}
When something in the search bar is typed(onKeyUp) add the listener handleInput.
Also create <RecipesList searchQuery={this.state.term}/>
now in the render function RecipesList filter out the recipes you want to display from your list