Passing an object from one component to another using props - javascript

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/

Related

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>
}

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

React - Array of children components not rerendering with props expression set in a callback

I understand that the wording of the question is slightly nebulous, so I will expand. This is a personal project of mine that I have taken up to learn some React basics and familiarize myself with socket.io
I have a CollapsibleList component, and a NestedList component, which renders an array of the CollapsibleList components.
NestedList has an event handler that gets set in componentWillMount of the component. The event is when a menu arrives via socket.io from my server. When the menu arrives, a new CollapsibleList is added to the array, and state is changed to trigger a rerender. The events are triggered by an initial socket.io event that is emitted via componentDidMount (get-menus).
CollapsibleList is collapsed/uncollapsed by its onclick which uses a toggleVisiblity method passed via props from the NestedList, whose state determines whether its child CollapsibleList components are open or not.
Problem: CollapsibleList props (which come from state of the NestedList) don't change on changing state of said NestedList. I have examined the properties in the debugger and I have been stuck for days. In other words, the CollapsibleList element appears on the browser window, but clicking it only changes the state of the NestedList, and the props of the CollapsibleList doesn't change, and thus it doesn't appear/disappear. I think it has something to do with creating the CollapsibleLists in the socket.io callback, bound with 'this', since the 'collapsed' prop of the CollapsibleList depends on this.state[restaurantId].collapsed. Source is below, if it is unclear I can add more explanation.
class CollapsibleList extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<List>
<ListItem
button
onClick={() => {
this.props.collapseEventHandler(this.props.restaurantId);
}}
>
<ListItemText primary="Collapse me!" />
</ListItem>
<ListItem>
<Collapse in={!this.props.collapsed} timeout="auto" unmountOnExit>
<ListItemText primary="Hello World!" />
</Collapse>
</ListItem>
</List>
);
}
}
class NestedList extends React.Component {
constructor(props) {
super(props);
//let menuData = props.menuData.map()
this.toggleVisiblity = this.toggleVisiblity.bind(this);
this.arrayOfMenus = [];
}
componentWillMount() {
socket.on(
"menu-arrived",
function(menuJson) {
if (menuJson.response.menu.menus.items) {
let restaurantId = menuJson.restaurant_id;
//let menuId = menuJson.response.menu.menus.items[0].menuId;
this.arrayOfMenus.push(
<CollapsibleList
collapsed={this.state[restaurantId].collapsed}
collapseEventHandler={this.toggleVisiblity}
restaurantId={restaurantId}
key={restaurantId}
/>
);
this.setState(function(prevState, props) {
return {
[restaurantId]: {
collapsed: prevState[restaurantId].collapsed,
updated: true
}
};
});
}
}.bind(this)
);
}
componentDidMount() {
getNearbyRestaurantRawData().then(
function(rawData) {
let restaurantIds = parseOutVenueIds(rawData);
let menusOpen = {};
for (let i = 0; i < restaurantIds.length; i++) {
menusOpen[restaurantIds[i]] = {
collapsed: true
};
}
this.setState(menusOpen, () => {
socket.emit("get-menus", {
ids: restaurantIds
});
});
}.bind(this)
);
}
toggleVisiblity(restaurantId) {
this.setState(function(prevState, props) {
let newState = Object.assign({}, prevState);
newState[restaurantId].collapsed = !prevState[restaurantId].collapsed;
return newState;
});
}
render() {
return (
<List>
<React.Fragment>
<CssBaseline>{this.arrayOfMenus}</CssBaseline>
</React.Fragment>
</List>
);
}
}
You are pushing CollapsibleList React elements to an array on the instance, which means that new React elements will not be created and returned from the render method when state or props change.
You should always derive your UI from state and props in the render method instead.
Example
class NestedList extends React.Component {
state = { restaurantIds: [] };
componentWillMount() {
socket.on("menu-arrived", menuJson => {
if (menuJson.response.menu.menus.items) {
let restaurantId = menuJson.restaurant_id;
this.setState(prevState => {
return {
restaurantIds: [...prevState.restaurantIds, restaurantId],
[restaurantId]: {
collapsed: prevState[restaurantId].collapsed,
updated: true
}
};
});
}
});
}
// ...
render() {
return (
<List>
<React.Fragment>
<CssBaseline>
{this.state.restaurantIds.map(restaurantId => (
<CollapsibleList
collapsed={this.state[restaurantId].collapsed}
collapseEventHandler={this.toggleVisiblity}
restaurantId={restaurantId}
key={restaurantId}
/>
))}
</CssBaseline>
</React.Fragment>
</List>
);
}
}

Programmatically open a route with state in react

I have two types of item, one of which can contain data similar to the other.
Currently when form is used to save an item it saves it then uses browserHistory.push to show the next page.
But I wish add a button that will
save the currently item
redirect them to the form to add the other item type,
partially fill out this form with the data from the first item.
Is there a way to do this using react and not using local storage or session variables?
You should take a look to Redux (or other Flux based libraries) to store data between components and routes, avoiding the excessive prop nesting.
browserHistory.push won't work. It only moves you to a certain location but it doesn't update the application state. You need to update application state, which then will reflect into location update, but not in the opposite direction. Keep in mind that, in React, data comes first, and its representation, even though mutable, doesn't change the data back. The same applies to the location.
To make the redirect alone work, I'd recommend wrapping your component into withRouter higher-order component.
import React, { Component } from 'react';
import { withRouter } from 'react-router';
class MyComponent extends Component {
render() {
return (
<div>
<button
onClick={() => this.props.router.push('/new-location')}>
Click me to go to /new-location
</button>
</div>
);
}
}
But if you need to pass data from one component to another, and the two aren't in hierarchy, I'd agree with Alomsimoy and recommend using Redux. But if, for some reason, it's not an option, you can store this data in a component that is parent to both forms:
class FormA extends Component {
render() {
return (
<form onSubmit={() => this.props.onSubmit()}>
<input
type="text"
value={this.props.inputA}
onChange={(event) => this.props.handleChangeA(event)} />
</form>
);
}
}
class FormB extends Component {
render() {
return (
<form onSubmit={() => this.props.onSubmit()}>
<input
type="text"
value={this.props.inputB}
onChange={(event) => this.props.handleChangeB(event)} />
</form>
);
}
}
while their parent would rule the location and state updates:
class Forms extends Component {
constructor() {
super();
this.state = {};
}
handleChange(name, value) {
this.setState({
[name]: value
});
}
renderForm() {
const {
params: {
stepId
}
} = this.props;
if (stepId === 'step-a') { // <- will be returned for location /form/step-a
return (
<FormA
inputA={this.state.inputA}
handleChangeA={(event) => this.handleChange('inputA', event.target.value)}
onSubmit={() => this.props.router.push('/form/step-b')} />
);
} else if (stepId === 'step-b') { // <- will be returned for location /form/step-b
return (
<FormB
inputB={this.state.inputB}
handleChangeB={{(event) => this.handleChange('inputA', event.target.value)} />
);
}
}
render() {
const {
children
} = this.props;
console.log(this.state); // track changes
return (
<div>
{this.renderForm()}
<button
onClick={() => this.props.router.push('/new-location')}>
Click me to go to /new-location
</button>
</div>
);
}
}
export default withRouter(Forms);
so the route for them would look like
<Route path="form/:stepId" component={Forms} />

Categories

Resources