Button click won't update state in my React app - javascript

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);

Related

Props are showing "undefined" when on click to send props to another component

I am building a simple react app and I am beginner. I am trying to send state as props to another component But when I access props like this.props.name then it is showing undefined.
App.js:
class Home extends React.Component {
state = {
fields : {
name : '',
}
}
updateField = evt => {
const fields = Object.assign({}, this.state.fields);
fields[evt.target.name] = evt.target.value;
console.log(evt.target.value)
this.setState({fields})
}
render() {
return(
<div>
<form>
<input
name="name"
id="name_id"
onChange={this.updateField}
value={this.state.fields.name}
/>
</form>
<Link to='/second_component'><SecondComponent name={this.state.fields} />Submit</Link>
<Routes>
<Route path='/second_component' element={<SecondComponent/>} />
</Routes>
</div>
)
}
}
class SecondComponent extends React.Component {
render() {
return (
<div style={{color: "white"}}>
{console.log(this.props.name)}
</div>
)
}
}
export default Home;
I have tried many times but it is still not working. When I try to console.log then it is showing "undefined".
Change fields[evt.target.name] = evt.target.value; to fields.name = evt.target.value; and name={this.state.fields} to name={this.state.fields.name}. Should work.

Refresh Component on route parameter change

Why the component doesn't update the props when changing the route param?
Trying to use setState inside componentWillReceiveProps but it doesn't even fire?
export default class HomePage extends React.PureComponent {
movies = require('../../movieList.json');
state = {
match: this.props.match,
id: null
}
componentWillReceiveProps(np) {
if(np.match.params.id !== this.props.match.params.id) {
console.log('UPDATING PROPS')
this.setState({ match: np.match })
}
}
render() {
const { match } = this.props;
const movie = this.movies[match.params.id || movieOrder[0]];
const nextMovie = getNextMovie(match.params.id || movieOrder[0]);
const prevMovie = getPrevMovie(match.params.id || movieOrder[0]);
return (
<>
<h1>{movie.title}</h1>
<Link to={nextMovie}>Next</Link>
<Link to={prevMovie}>Prev</Link>
</>
);
}
}
nextMovie and prevMovie get the id which should be set inside link. Unfortunatelly it sets only during 1st render. When clicked I can see the url change, but no updates are fired
Here is the component holding the switch
export default class App extends React.PureComponent {
state = {
isLoading: true,
}
componentDidMount() {
PreloaderService.preload('core').then(() => {
console.log('LOADED ALL');
console.log('PRELOADER SERVICE', PreloaderService);
this.setState({ isLoading: false });
console.log('Preloader assets', PreloaderService.getAsset('dog-video'))
});
}
render() {
const { isLoading } = this.state;
if(isLoading) {
return(
<div>
is loading
</div>
)
}
return (
<div>
{/* <Background /> */}
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path="/:id" component={props => <HomePage {...props} />} />
<Route component={NotFoundPage} />
</Switch>
<GlobalStyle />
</div>
);
}
}```
Seems to be working with my codesandbox recreation.
I had to improvise a bit with the missing code:
What's the value of movieOrder[0]? Is 0 a valid value for match.params.id?
How do getNextMovie and prevMovie look like?
Also, you're not using the Home component's state in your render logic, so I think, it should render the correct movie even without them.
There are several things off here. componentWillReceiveProps is considered unsafe and is deprecated. Try using componentDidUpdate instead. Also your class component needs a constructor. As it is now state is defined as a static variable.
constructor(props) {
super(props);
this.state = {
match: this.props.match,
id: null
}
}
componentDidUpdate(np) {
if(np.match.params.id !== this.props.match.params.id) {
console.log('UPDATING PROPS')
this.setState({ match: np.match })
}
}
Last, double check to make sure your logic is correct. Right now the Next and Prev buttons link to the same thing:
const nextMovie = getNextMovie(match.params.id || movieOrder[0]);
const prevMovie = getPrevMovie(match.params.id || movieOrder[0]);
<Link to={nextMovie}>Next</Link>
<Link to={prevMovie}>Prev</Link>
As an alternative, if you want to stick with componentWillReceiveProps you need to unbind the current movie, and rebind it with a new one. Here is a code example doing the exact same thing with users.

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

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.

ReactJS instant Search with input

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

Re-render the component with new data without having local state in React

I'm practicing react and redux and I'm creating a simple app where I have a sidebar showing a list of categories that is visible on every route and the main area that initially displays all the books I have and when clicking on a category link on the sidebar the main area loading another component with all the books related to this category.
Here's my routes setup in the App.js file ...
class App extends Component {
async componentDidMount() {
try {
await this.props.asyncLoadBooks();
await this.props.asyncLoadCategories();
} catch (error) {
console.log(error);
}
}
render() {
return (
<>
<Header />
<div className="global-wrapper">
<div className="container">
<aside className="side-bar">
<Categories />
</aside>
<main className="main-content">
<Switch>
<Route exact path="/" component={Books} />
<Route
exact
path="/category/:id"
component={Category}
/>
<Route component={NotFound} />
</Switch>
</main>
</div>
</div>
</>
);
}
}
In the App.js as you can see I'm loading the data via a local JSON file with axios in the Actions files of the booksActions and categoriesAction, it's pretty straightforward.
And here's the Categories component ...
class Categories extends Component {
render() {
const { categories } = this.props;
let categoriesList;
if (categories && categories.length !== 0) {
categoriesList = categories.map(category => (
<li key={category.id}>
<Link to={`/category/${category.id}`}>{category.name}</Link>
</li>
));
} else {
categoriesList = <Loading />;
}
return (
<div>
<h2>Categories</h2>
<ul>{categoriesList}</ul>
</div>
);
}
}
const mapState = state => ({
categories: state.categories.categories
});
export default connect(mapState)(Categories);
And I'm firing another action in the ComponentDidMount() of the single Category component to get all the books related to that component and render them ...
class Category extends Component {
componentDidMount() {
this.props.getCategoryBooks(this.props.match.params.id);
}
componentDidUpdate(prevProps) {
if (prevProps.match.params.id !== this.props.match.params.id) {
this.props.getCategoryBooks(this.props.match.params.id);
}
}
render() {
const { categoryBooks } = this.props;
return (
<div>
{/* <h1>{this.props.match.params.id}</h1> */}
{categoryBooks &&
categoryBooks.map(book => {
return <div key={book.id}>{book.title}</div>;
})}
</div>
);
}
}
const mapState = state => ({
categories: state.categories.categories,
categoryBooks: state.books.categoryBooks
});
const mapActions = {
getCategoryBooks
};
export default connect(
mapState,
mapActions
)(Category);
Now, everything is working the first time, however, when I click on another category the <Category /> component doesn't get updated because I'm dispatching the action in the componentDidMount() thus the component already mounted the first time, so it doesn't dispatch the action again after I click on another category, now what is the best way to handle this?
The second issue is where I'm on a category route http://localhost:3000/category/9967c77a-1da5-4d69-b6a9-014ca20abd61 and I try to refresh the page, the categoris list loads fine on the sidebar, but the single component shows empty, and when I look on the redux-devtools I find that the GET_CATEGORY_BOOKS action gets fired before the LOAD_BOOKS and LOAD_CATEGORIES in the App.js file, because the child componentDidMount() method gets called before its parent equivalent method. How to solve this as well?
I hope you guys can help me in this.
Edit
As ##NguyễnThanhTú noticed, the componentDidupate had a typo, now it works when clicking on another category.
That leaves us with the second issue when reloading the page in the category route and the data doesn't show because of the App.js componentDidMount fires after its children components.
Edit
Here's a repo on Github for this project ...
https://github.com/Shaker-Hamdi/books-app
In your booksActions.js, add this:
export const getCategoryBooksV2 = categoryId => {
return async (dispatch, getState) => {
const { books } = getState();
if (books.books.length === 0) {
console.log('Only executing once') // testing purpose only
const response = await axios.get("books.json");
const data = response.data.books;
dispatch(loadBooks(data));
dispatch(getCategoryBooks(categoryId));
}
dispatch(getCategoryBooks(categoryId));
};
};
In your Category.js, use that new action creator:
import { getCategoryBooksV2 } from "../books/booksActions";
...
componentDidMount() {
this.props.getCategoryBooksV2(this.props.match.params.id);
}
...
const mapActions = {
getCategoryBooksV2
};
This solution is inspired by this example:
function incrementIfOdd() {
return (dispatch, getState) => {
const { counter } = getState();
if (counter % 2 === 0) {
return;
}
dispatch(increment());
};
}
From the Redux-Thunk Documentation
This is the demo:

Categories

Resources