React Component receive props but doesn't render it, why? - javascript

I have a page displaying user's books.
On this MyBooks page, React component mount. When it's mounted it fetch user's books through API. Then it update component's state with user's books.
mount component
fetch books through API
when we have results, update component's state
render again BooksList component (but it's not happening)
Here is my code for MyBooks component :
class MyBooks extends Component {
// TODO: fetch user info
constructor(props) {
super(props);
this.state = {
books: [],
errors: []
};
this.fetchBooks = this.fetchBooks.bind(this);
}
componentDidMount() {
console.log('component mounted!');
this.fetchBooks();
}
fetchBooks() {
let _this = this;
BooksLibraryApi.getBooks().then(foundBooks => {
console.log('books found:', foundBooks);
_this.setState({
books: foundBooks
});
});
}
render() {
console.log('MyBooks state:', this.state);
return (
<Section>
<Container>
<h1>My books</h1>
<BooksList books={this.state.books} />
</Container>
</Section>
);
}
}
export default withRouter(MyBooks);
Here is the result for console.log('books found:', foundBooks):
Here is my code for BooksList component :
class BooksList extends React.Component {
render() {
console.log('BooksList props:', this.props);
return (
<Columns breakpoint="mobile">
{this.props.books.map((book, i) => {
console.log(book);
return (
<Columns.Column
key={i}
mobile={{ size: 'half' }}
desktop={{ size: 2 }}
>
<BookCard book={book} />
</Columns.Column>
);
})}
</Columns>
);
}
}
export default BooksList;
Here is the code for BookCard component:
class BookCard extends React.Component {
constructor(props) {
super(props);
console.log('props', props);
this.readBook = this.readBook.bind(this);
this.addBook = this.addBook.bind(this);
this.deleteBook = this.deleteBook.bind(this);
this.wantBook = this.wantBook.bind(this);
}
readBook() {
BooksLibraryApi.readBook(this.props.book.id);
}
addBook() {
BooksLibraryApi.addBook(this.props.book.id);
}
wantBook() {
BooksLibraryApi.wantBook(this.props.book.id);
}
deleteBook(e) {
BooksLibraryApi.deleteBook(this.props.book.id, e);
}
render() {
return (
<div className="card-book">
<Link to={`/book/${this.props.book.id}`}>
{this.props.book.doHaveThumbnail ? (
<Image
alt="Cover"
src={this.props.book.thumbnailUrl}
size={'2by3'}
/>
) : (
<div className="placeholder">
<span>{this.props.book.title}</span>
</div>
)}
</Link>
<Button fullwidth color="primary" size="small" onClick={this.wantBook}>
Add to wishlist
</Button>
</div>
);
}
}
export default withRouter(BookCard);
The console.log in BooksList component is not called. Which means that the component is render only one time, when the this.props.books array is empty.
I don't understand why BooksList is not rendered again when his props are updated (when MyBooks component has his state updated).
Strange behavior: I'm using React Router, and when I first click on the link "My books" (which go to my MyBooks component), it doesn't work, but when I click again on it, everything works fine. Which means that something is wrong with rendering / component's lifecyles.
Thanks.

Related

unable to find a React.Component by id

I have a React.Component with render() declared this way:
render(){
return <div>
<button id="butt" onClick={()=> $("#noti").change("test") }>click me</button>
<Notification id="noti" onMounted={() => console.log("test")}/>
</div>
}
And this is my Notification class:
class Notification extends React.Component {
constructor(props) {
super(props)
this.state = {
message: "place holder",
visible: false
}
}
show(message, duration){
console.log("show")
this.setState({visible: true, message})
setTimeout(() => {
this.setState({visible: false})
}, duration)
}
change(message){
this.setState({message})
}
render() {
const {visible, message} = this.state
return <div>
{visible ? message : ""}
</div>
}
}
As the class name suggests, I am trying to create a simple notification with message. And I want to simply display the notification by calling noti.show(message, duration).
However, when I try to find noti by doing window.noti, $("#noti") and document.findElementById("noti"), they all give me undefined, while noti is displayed properly. And I can find the butt using the code to find noti.
How should I find the noti? I am new to front end so please be a little bit more specific on explaining.
It's not a good idea using JQuery library with Reactjs. instead you can find a appropriate react library for notification or anything else.
Also In React we use ref to to access DOM nodes.
Something like this:
constructor(props) {
super(props);
this.noti = React.createRef();
}
...
<Notification ref={this.noti} onMounted={() => console.log("test")}/>
more info: https://reactjs.org/docs/refs-and-the-dom.html
I have hardcoded the id to 'noti' in the render method. You can also use the prop id in the Notification component.I have remodelled the component so that you can achieve the intended functionality through React way.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
messageContent: 'placeholder'
}
}
setMessage = (data) => {
this.setState({messageContent : data});
}
render() {
return (
<div className="App">
<button id='butt' onClick= {() => this.setMessage('test')} />
<Notification message = {this.state.messageContent} />
</div>
);
}
}
class Notification extends React.Component {
render () {
const {message} = this.props;
return (
<div id='noti'>
{message}
</div>
)
}
}
Before beginning: Using id/class to reach DOM nodes is not suggested in React.js, you need to use Ref's. Read more at here.
In your first render method, you give id property to Notification component.
In react.js,
if you pass a property to some component, it becomes a props of that
component. (read more here)
After you give the id to Notification, you need to take and use that specific props in your Notification component.
You see that you inserted a code line super(props) in constructor of Notification? That means, take all the props from super (upper) class and inherit them in this class.
Since id is HTML tag, you can use it like:
class Notification extends React.Component {
constructor(props) {
// inherit all props from upper class
super(props);
this.state = {
message: "place holder",
visible: false,
// you can reach all props with using this.props
// we took id props and assign it to some property in component state
id: this.props.id
}
}
show(message, duration){
// code..
}
change(message){
// code..
}
render() {
const {visible, message, id} = this.state
// give that id to div tag
return <div id={id}>
{message}
</div>
}
}
You can't pass id/class to a React Component as you would declare them in your normal HTML. any property when passed to a React Component becomes a props of that component which you have to use in the component class/function.
render() {
const {visible, message} = this.state
// give your id props to div tag as id attr
return <div id={this.props.id}>
{message}
</div>
}
This answer does not provide the exact answer about selecting a component as you want. I'm providing this answer so you can see other alternatives (more React way maybe) and improve it according to your needs.
class App extends React.Component {
state = {
isNotiVisible: false
};
handleClick = () => this.setState({ isNotiVisible: true });
render() {
return (
<div>
<button onClick={this.handleClick}>Show Noti</button>
{this.state.isNotiVisible && (
<Noti duration={2000} message="This is a simple notification." />
)}
</div>
);
}
}
class Noti extends React.Component {
state = {
visible: true
};
componentDidMount() {
setTimeout(() => this.setState({ visible: false }), this.props.duration);
}
render() {
return this.state.visible && <div>{this.props.message}</div>;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />

Passing a variable between non-nested components using Context API

Suppose I have two components which aren't nested: a button and a panel. When the button is clicked, the panel will show or hide depending on the previous state (like an on/off switch). They aren't nested components, so the structure looks like this:
<div>
<Toolbar>
<Button />
</Toolbar>
<Content>
...
<ButtonPanel />
</Content>
</div>
I can't change the structure of the DOM. I also can't modify any other component other than the button and panel components.
The Button and ButtonPanel components are related, however, and will be used together throughout the solution. I need to pass a property to the panel to let it know when to show or when to hide. I was thinking about doing it with Context API, but I think there's something I'm doing wrong and the property never updates.
This is my code:
Context
import React from 'react';
export const ButtonContext = React.createContext({
showPanel: false,
});
Button
import React, { Component } from 'react';
import { ButtonContext } from './ButtonContext';
class Button extends Component {
constructor() {
super();
this.state = {
showPanel: false,
};
}
render() {
return (
<ButtonContext.Provider value={{ showPanel: this.state.showPanel }}>
<li>
<a
onClick={() => this.setState({ showPanel: !this.state.showPanel }, () => console.log('Changed'))}
>
<span>Button</span>
</a>
</li>
</ButtonContext.Provider>
);
}
}
export { Button };
Panel
import React, { Component } from 'react';
import { Panel, ListGroup, ListGroupItem } from 'react-bootstrap';
import { ButtonContext } from './ButtonContext';
class ButtonPanel extends Component {
static contextType = ButtonContext;
render() {
return (
<ButtonContext.Consumer>
{
({ showPanel }) => {
if (showPanel) {
return (
<Panel id="tasksPanel">
<Panel.Heading >Panel Heading</Panel.Heading>
<ListGroup>
<ListGroupItem>No Items.</ListGroupItem>
</ListGroup>
</Panel>
);
}
return null;
}
}
</ButtonContext.Consumer>
);
}
}
export { ButtonPanel };
I've also tried simply accessing the context in the ButtonPanel component like so:
render() {
const context = this.context;
return context.showPanel ?
(
<Panel id="tasksPanel">
<Panel.Heading >Tasks</Panel.Heading>
<ListGroup>
<ListGroupItem className="tasks-empty-state">No tasks available.</ListGroupItem>
</ListGroup>
</Panel>
)
:
null;
}
What am I doing wrong?
Thanks
From the React docs:
Accepts a value prop to be passed to consuming components that are descendants of this Provider.
So this means that <ButtonContext.Provider> has to wrap <ButtonContext.Consumer> or it has to be higher up in the component hierarchy.
So based on your use case, you could do:
// This app component is the div that wraps both Toolbar and Content. You can name it as you want
class App extends Component {
state = {
showPanel: false,
}
handleTogglePanel = () => this.setState(prevState => ({ togglePanel: !prevState.togglePanel }));
render() {
return (
<ButtonContext.Provider value={{ showPanel: this.state.showPanel, handleTogglePanel: this.handleTogglePanel }}>
<Toolbar>
<Button />
</Toolbar>
<Content>
<ButtonPanel />
</Content>
</ButtonContext.Provider>
);
}
}
class Button extends Component {
...
<ButtonContext.Consumer>
{({ handleTogglePanel }) => <a onClick={handleTogglePanel} />}
</ButtonContext.Consumer>
}
class ButtonPanel extends Component {
...
<ButtonContext.Consumer>
{({ showPanel }) => showPanel && <Panel>...</Panel>}
</ButtonContext.Consumer>
}

Passing an object from one component to another using props

I am trying to pass a whole object from one component to another on button click.
I have tried using Link and Router, but I still cannot see the object inside the props of the destination component.
I am using the onClick handler handleReportIncident to pass an available object while Routing to this component. The handleReportIncident function is bound at the top.
handleReportIncident() {
const { trip } = this.state;
this.props.history.push(`/app/new`)
return (
<div>
<New trip={trip} />
</div>
)
}
render() {
return (
<Button
variant="contained"
color="primary"
className={classes.toolbarButton}
onClick={this.handleReportIncident}
>
<AddIcon />
Incident
</ Button>
)}
Inside of New component I have :
class New extends Component {
render() {
const {trip} = this.props;
console.log("the trip is", trip);
return(
...
)
}
New.propTypes = {
trip: PropTypes.object.isRequired,
};
Note: I am using material UI as a theme.
The Log inside the New component is the trip is undefined.
You can wrap your desired object in the state of the Link. See example below.
// Main Component from which you want object passed
render() {
return (
<Link to = {{
pathname: '/app/new',
state: {
trip: this.state.trip
}
}}>
<AddIcon /> Incident
</Link>
)
}
// New Component get desired object from this.props.location.state
class New extends Component {
render() {
const { trip } = this.props.location.state
console.log("the trip is", trip);
return(
...
)
}
New.propTypes = {
trip: PropTypes.object.isRequired,
}
For more info on how it works head over to this tutorial: https://tylermcginnis.com/react-router-pass-props-to-link/

Adding state to the end of another components state, only when a button is pushed

I have three components:
PageBuilder - is basically a form where the user adds a page name and selects some items.
PageList - stores all pages the user has created in state and renders that state as a list
PageUpdater - takes the form info from PageBuilder and adds it to PageList
When someone clicks a button in PageBuilder, PageBuilder pushes its state to PageUpdater:
// PageBuilder
addNewPage = () => {
let info = {...this.state};
this.props.callBack(info);
}
render(){
return <button onClick={this.addNewPage}>New Page</button>
}
PageUpdater receives state from PageBuilder and passes it down to PageList as props
// PageUpdater
constructor(props){
super(props);
this.state={
data: ''
}
}
updatePageList = (pageAdded) =>{
this.setState({data:pageAdded});
}
render(){
return(
<div>
<PageBuilder callBack={this.updatePageList}/>
<PageList addToList={this.state.data} />
</div>
)
}}
PageList receives the object in props and adds it to its own state. It then returns that state as a list
// PageList
constructor(props){
super(props);
this.state = {pages:''};
}
componentWillReceiveProps(nextProps) {
this.setState({pages: [...this.state.pages, nextProps.addToList]});
}
getPages = () => {
var pages = []
for(var key in this.state.pages){
pages.push(this.state.pages[key].pageTitle)}
return pages; // Return an array with the names
}
render(){
return(
<div>
{this.getPages().map((page, index) => <li key={index}> {page} </li>)}
</div>
)
}}
The problem is that I'm using componentWillReceiveProps to tell PageList when to update its state. So every time the props change, PageUpdaters state is added to the PageList. I only want this to happen when the user clicks the new page button (in the PageBuilder).
One of the main principles of React is that every state is managed by one component, you change and manage your list through three components. Instead your pagelist should be a stateless component:
const PageList = ({ pages }) => (<div>
{ pages.map(({ pageTitle }, index) => <li key={index} >{pageTitle}</li> ) }
</div>);
Now Page should contain the pages in its state and provide a method to change it:
class Page extends React.Component {
state = { pages: [] };
addPage = (page) => {
this.setState(({ pages }) => ({ pages: pages.concat(page) }));
}
render() {
return (<div>
<PageList pages={this.state.pages} />
<PageBuilder add={this.addPage} />
</div>);
}
}
Now the PageBuilder only has to call this.props.add({ pageTitle: "something" }) ...

Button click won't update state in my React app

A button click shall filter my job-card array to only one category. E.g. button "Marketing" should filter to those jobs from array who have prop "jobstags: Marketing". I used a very similar procedure like for my input which filters jobs perfectly.
I can console log my event (the button click) with the according value ("Marketing"). But it still doesn't filter correctly...
In my app I did this:
export default class App extends Component {
state = {
jobs: jobs,
searchfield: '',
jobtags: ''
}
onSearchChange = event => {
this.setState({ searchfield: event.target.value })
}
onClickChange = event => {
console.log(event.target.value)
this.setState({ jobtags: event.target.value })
}
render() {
const filteredJobs = this.state.jobs.filter(job => {
return (
job.position
.toLowerCase()
.includes(this.state.searchfield.toLowerCase()) ||
job.company
.toLowerCase()
.includes(this.state.searchfield.toLowerCase()) ||
job.jobtags.toLowerCase().includes(this.state.jobtags.toLowerCase())
)
})
// this.save()
if (this.state.jobs.length === 0) {
return <Loading>Loading...</Loading>
} else {
return (
<Router>
<React.Fragment>
<Route
exact
path="/"
render={() => (
<Home
jobs={filteredJobs}
searchChange={this.onSearchChange}
clickChange={this.onClickChange}
/>
)}
/>
onClickChange is what should update the state of tags
In my Home component I then simply pass the value on to the Categories component:
<Categories clickChange={clickChange} />
Finally it arrives in my Categories component where I say:
export default class Categories extends Component {
render() {
const { clickChange } = this.props
return (
<Wrapper>
<button value="Marketing" onClick={clickChange}>
<img
alt="Button"
src={require('/Users/markus/Documents/q4-2018/jobs-app/src/img/computer.png')}
/>
Frontend
</button> ...
Any ideas? Thx!
maybe you have to bind the "this" of "onClickChange", for example in the constructor of your App class.
Example :
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
jobs: jobs,
searchfield: '',
jobtags: ''
};
this.onClickChange = this.onClickChange.bind(this);
and it will work I think
You will have to bind it. Add this line to your constructor:
this.onClickChange = this.onClickChange.bind(this);

Categories

Resources