I am using react hooks and attempting to pass the state of a parent to multiple components. The problem is when the components which I want to pass the state to are themselves part of a state. I've created an example which illustrates my problem, I have a button which increments a counter and appends another button to a list. I'd like to see the list of buttons displayed and all reading the same counter value from the state, but when I append a new button, the value of the counter is always passed by value at the time of creating the button, and it does not update. In my project (not shown here) I've circumvented this problem by passing functions to the components to retrieve the parent state and manually update all the components, however by doing this I feel that I am really breaking the design principles of react with unnecessarily complex code which leads to a lot more problems.
my example with buttons:
import React from 'react'
function Button(props){
return <button onClick={()=>{
// increment the counter
props.setcounter(cur=>cur+1);
// add another button
// these props are not passed by reference: how do I pass these by reference?
props.setitems(items=>items.concat(<tr><td><Button {...props}/></td></tr>))
}}>
{props.counter}
</button>
}
function App() {
const [items,setitems]=React.useState([]);
const [counter,setcounter]=React.useState(0);
return (
<div className="App">
<table>
<th><td>Button with props passed by reference:</td></th>
<tr><td><Button counter={counter} setcounter={setcounter} setitems={setitems} /></td></tr>
</table>
<table>
<th><td>Button with props passed by value:</td></th>
{items}
</table>
</div>
);
}
export default App;
function Button(props){
return <button onClick={()=>{
// increment the counter
props.setcounter(cur=>cur+1);
// add another button
// these props are not passed by reference: how do I pass these by reference?
props.setitems(items=>items.concat(<tr><td><Button {...props}/></td></tr>))
}}>
{props.counter}
</button>
}
function App() {
const [items,setitems]=React.useState([]);
const [counter,setcounter]=React.useState(0);
return (
<div className="App">
<table>
<th><td>Button with props passed by reference:</td></th>
<tr><td><Button counter={counter} setcounter={setcounter} setitems={setitems} /></td></tr>
</table>
<table>
<th><td>Button with props passed by value:</td></th>
{items}
</table>
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
what it does:
When I click the top button multiple times, it creates a new button every time with the current value of the counter, but it is not passed by reference and thus does not update when the counter value updates.
what I want it to do:
All the newly created buttons should have the same counter value because I want it to be passed by reference, not the value at the time I click the button. When I click any of the buttons, a new button should be created and the value on every button should be updated to match the new counter value.
The problem is when the components which I want to pass the state to are themselves part of a state.
This is indeed the problem, and an anti-pattern in React which as you've discovered creates stale values. Ideally you should not be keeping any components in state, and if you do, they must be completely pure (i.e. not liable to recieve new props during their life cycle).
What's actually happening when you call this state setter:
props.setitems(items=>items.concat(<tr><td><Button {...props}/></td></tr>))
Is this, assuming it's the first time its being called:
<Button counter={0} ... />
Not only will that button only ever have the value 0 as its counter, but any buttons it creates in turn will have the same value. What you're talking about doesn't actually make sense in a React context because the whole point is to break referentially (and strict equality more generally) in order to trigger rerenders and effects.
We want to keep our state immutable which means recreating any parts of it which change on every render. For what you're doing to work, you would have to recreate the entire store of buttons by looping as many times as the length of items whenever you create a new one.
The way you should be thinking about this is how to derive the UI from data, not store it as data:
function Button(props){
return <button onClick={()=>{
props.setcounter(cur=>cur+1);
props.setitems(items=> [...items, 1])
}}>
{props.counter}
</button>
}
function App() {
const [items,setitems]=React.useState([1]);
const [counter,setcounter]=React.useState(0);
return (
<div className="App">
{items.map(() => <Button counter={counter} setcounter={setcounter} setitems={setitems}/>)}
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Related
So basically I have two components. In Component 1 there is an img. In component 2 there is a button. I need when i press the button img disappears and also button disappears.
const Icons=()=> {
return (
<div className="_icons">
<div className="icons__Top">
<img src="./icons/-48.png" alt="Twitter"/>
<figcaption>Whatever</figcaption>
</div>
</div>
const Button= () => {
return (
<div className="button">
<Button variant="outlined" className="button__rightpage" >REMOVE</Button>
<caption className="text" ></caption>
</div>
)
}
There can be more than one solution,
1)I think you need to have one parent component for both components and make one state in parent component and then pass it in icon component and give it to img and default make it to false and then from button component change that state to true.
2)without parent component -> You can also use context api to change value directly from button component and hide img.
In reviewing and making my code more modular and robust, as any programmer should, I noticed I was using a similar button component multiple times. As such I decided to create a button component and just render it with the new route link and text as in the page rendered.
I'm completely new to react (~ 5 days in learning) with a fairly well versed programming background.
Simple component button, I use react-route-dom : Link prop to route to new pages.
function ActionButton () {
return (
<div className="Action">
<button className="ActionButton">
<Link to={this.props.navLink}>
{this.props.text}
</Link>
</button>
</div>
);
}
using/constructing of the button component
function ActionPage () {
return (
<div className="ActionPage">
<ActionButton
navLink="/urlLink1"
text="btn1"
/>
<ActionButton
navLink="/urlLink2"
text="btn2"
/>
</div>
);
}
this doesn't work, I get the following:
TypeError: Cannot read property 'props' of undefined
When using a stateless functional component as you are (as opposed to a class based one) the component is called with ComponentName(props) - you can access props by updating the signature of the component to:
ActionButton (props) {
Which will allow you to access props.navLink etc inside the function.
Your ActionButton component is a dump component, so you have to pass the props as a argument to the function. Update your ActionButton component as shown below.
function ActionButton (props) {
return (
<div className="Action">
<button className="ActionButton">
<Link to={props.navLink}>
{props.text}
</Link>
</button>
</div>
);}
As you are new to React, read more about dump vs smart components here: https://medium.com/#thejasonfile/dumb-components-and-smart-components-e7b33a698d43
I'm having a hard time wrapping my head around this issue. I was wondering if someone here would be able to take a look? I have a component that I'm passing a method called this.fetchContent to as props called Filter. this.fetchContent fires an action creator that uses axios via Redux to get some data from an API, I pass it the current page location using react-router viathis.props.params.
The issue occurs with calling the method from the child component. What I've done is bound this.props.fetchContent to an onClick handler inside the Filter component. Whenever I click the <Link /> tag the function fires, and the page route gets updated. However the value of the parent components props doesn't update until after the function has fired, causing only every other click to produce the correct API call.
App:
class App extends Component {
constructor(props) {
super(props)
this.fetchContent = this.fetchContent.bind(this);
}
static contextTypes = {
router: PropTypes.object
};
componentDidMount() {
this.fetchContent();
}
fetchContent() {
let query = `${this.props.params.sub}/${this.props.params.filter}`;
this.props.fetchList(query, this.props.location.search);
}
render() {
return (
<div>
<Filter
filter={this.props.params.filter}
sub={this.props.params.sub}
search={this.props.location.search}
fetchContent={this.fetchContent} />
{React.cloneElement(this.props.children, this.props)}
</div>
);
}
}
Filter
class Filter extends Component {
render() {
return (
<div className="mdl-tabs mdl-js-tabs mdl-js-ripple-effect">
<div className="mdl-tabs__tab-bar">
<Link to={`/r/${this.props.sub}/new/${this.props.search}`} onClick={this.props.fetchContent}>
<span className="mdl-tabs__tab is-active">Hot</span>
</Link>
</div>
</div>
)
}
}
I understand what's happening here but I'm not sure what the React-friendly way of solving this issue is. How can I re-factor my code to produce the results I need and what are the best practices for solving this sort of issue?
Edit: Updated the syntax but still seeing the same issue.
You have some syntax mistake I believe. You are executing the function rather than returning it.
This
<Link to={`/r/${this.props.sub}/new/${this.props.search}`} onClick={this.props.fetchContent()}>
<span className="mdl-tabs__tab is-active">Hot</span>
</Link>
Should be like this;
<Link to={`/r/${this.props.sub}/new/${this.props.search}`} onClick={this.props.fetchContent}>
<span className="mdl-tabs__tab is-active">Hot</span>
</Link>
OR like this
<Link to={`/r/${this.props.sub}/new/${this.props.search}`} onClick={() => { this.props.fetchContent() }}>
<span className="mdl-tabs__tab is-active">Hot</span>
</Link>
PS: I believe you have a typo on App component since you don have a closing on Filter component at render.
I am trying to learn React. I already have a good grasp of javascript. I am trying to learn by creating a small app that is basically a task manager. In my case it's for grocery related items. I have a fiddle created here. Could you please take a look at how I composed the react code and let me know if this is the best approach to building with components/classes? You can see that I have nested components. I am not sure if there is a better way of doing this.
Finally, I wan't a new "add-item-row" created every time a user clicks on the big blue Add button. Right now one is showing be default but I don't want any showing by default. I want one created (add-item-row, div) only when a user clicks on the Add button.
Here is the fiddle.
https://jsfiddle.net/j0mpsbh9/4/
<div id="app" class="container">
<script type="text/babel">
var AddItemWrapper = React.createClass({
render: function() {
return (
<div>
<div className="row">
<AppTitle />
<AddItemForm />
</div>
<AddItemRow />
</div>
);
}
});
var AppTitle = React.createClass({
render: function() {
return (
<div>
<h1>Grocery List</h1>
</div>
);
}
});
var AddItemForm =React.createClass({
render: function() {
return (
<div>
<div className="col-sm-6 col-lg-6">
<div className="form-group">
<label htmlFor="enter-grocery-item" className="sr-only">Enter Grocery Item</label>
<input type="text" className="form-control" id="enter-grocery-item" placeholder="Enter Grocery Item" />
</div>
</div>
<div className="col-sm-6 col-lg-6">
<button type="button" id="add" className="btn btn-block btn-info">Add <span className="glyphicons circle_plus"></span></button>
</div>
</div>
);
}
});
var AddItemRow =React.createClass({
render: function() {
return (
<div className="add-item-row">
<div className="row">
<div className="col-sm-12 grocery-items">
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="grocery-item" className="sr-only">Grocery Item</label>
<input type="text" className="form-control" id="grocery-item" placeholder="" />
</div>
</div>
<div className="col-sm-6 center">
<button type="button" className="btn btn-blockx btn-warning"><span className="glyphicons pencil"></span></button>
<button type="button" className="btn btn-blockx btn-lgx btn-danger"><span className="glyphicons remove"></span></button>
<button type="button" className="btn btn-blockx btn-lgx btn-success"><span className="glyphicons thumbs_up"></span></button>
</div>
</div>
</div>
</div>
);
}
});
ReactDOM.render(
<AddItemWrapper />,
document.getElementById('app')
);
</script>
</div>
You have a good start here! Your component hierarchy is organized in a sensible way. However you are missing any kind of interactivity or internal state.
The main way you make React components interactive is by using state plus event callbacks which modify said state. "State" is pretty self explanatory - it describes values inherent to how the components looks and behaves, but which change over time. Every time a React component's state is altered (with this.setState()) that component will re-render (literally by re-running the render() function) to reflect the changes.
First let's edit your AddItemWrapper class to keep track of some internal state when it is first mounted. We know that you want to have multiple rows of data, so let's give it an empty array to store future information about rows:
getInitialState(){
return {rows: []};
},
Now instead of rendering a single AddItemRow directly, we'll render a dynamic set of rows that is based on the current component state. Array.map() is perfect for this and a common use case in React:
{this.state.rows.map(function(ea, i){
return <AddItemRow initialItemName={ea} key={ea + "-" + i} />
})}
Basically what that does is take every entry in the array AddItemWrapper.state.rows array and renders it as an AddItemRow component. We give it two properties, initialItemName and key. "initialItemName" will just tell the child component what its name was when it was first added, and "key" is a unique string that allows React to differentiate components from their siblings.
Now we've set up AddItemWrapper to properly render rows based on its internal state. Next we have to modify AddItemForm so that it will react to user input and trigger new rows being added.
In AddItemForm, first we need to add a "ref" to the input text box. This is so that React can identify and read data from this HTML element after it is rendered:
<input ref={function(el){this.inputElement = el;}.bind(this)} ... />
Then give the button element a callback that will trigger when it's clicked:
<button onClick={this.handleClick} ... />
Finally write the callback handler itself:
handleClick(){
this.props.onAdd(this.inputElement.value);
this.inputElement.value = "";
}
Notice how this callback is calling this.props.onAdd()? That means we need to pass in a callback function from the parent (AddItemWrapper) to this component to use. This is how we communicate between parents and children in React: pass a function from a parent to a child which will be triggered from within the child, but will effect the parent.
In AddItemWrapper we make sure AddItemForm has access to the callback function:
AddItemForm onAdd={this.onAdd} />
And then we write the callback function itself:
onAdd(newItem){
var newRows = this.state.rows.slice();
newRows.push(newItem);
this.setState({rows: newRows});
}
Notice how we're copying the old array held in state (using Array.slice()), push a new item into the new array, and then update state with the new array? Never mutate state directly; ALWAYS copy it, modify the copy, and then update state with the new copy.
Almost done. We've created a way for AddItemWrapper to render its rows, and a way for AddItemForm to create new rows. Now we edit AddItemRow to render in a way that maintains its own internal state too.
First make sure it initializes its own state when it's mounted. We'll have it keep track of a string value, which initially is the same as what the user entered into the text box when they pressed "Add", but because it's kept in AddItemRow.state it can be modified later by the user:
getInitialState() {
return {itemName: this.props.initialItemName}
}
Now that the row name is kept in the component state, we can render it in the HTML like this:
<input value={this.state.itemName} ... />
Here's what it looks like when you put it all together!
There are obviously more features that you would want to add, such as letting the user edit, move, or delete a row entry. I'll leave those exercises up to you. I highly recommend you read through all of the official documentation as well as do a few tutorials to get your head in the game. It's obvious that you have a bit of experience under your belt given what you had so far, but getting the hang of how state, render(), and callbacks work takes some practice. Good luck!
When I add a tab with react, its parent element is already upgraded. So calling upgradElement has no effect and the added tab doesn't work.
What solution, recreate all tabs with the container and upgrade it ? React just update DOM component in this case I need to dismount component ?
How about calling componentHandler.upgradeElement() or componentHandler.upgradeDom() in the componentDidUpdate phase of the specific component
componentDidUpdate() {
componentHandler.upgradeElement(this.refs.myElement);
//or componentHandler.upgradeDom('MaterialTabs');
}
EDIT 1 Tab Component
componentDidUpdate() {
componentHandler.upgradeDom();
}
newTab() {
this.setState({
newtab: 1
});
}
render() {
return (<div className="mdl-layout mdl-js-layout mdl-layout--fixed-header" key={this.state.newtab}>
<button onClick={this.newTab.bind(this)}>Add Tab</button>
<header className="mdl-layout__header">
<div className="mdl-layout__tab-bar mdl-js-ripple-effect">
<Link to="/tabtest" hash="#scroll-tab-1" className="mdl-layout__tab is-active">Tab 1</Link>
<Link to="/tabtest" hash="#scroll-tab-2" className="mdl-layout__tab">Tab 2</Link>
{ this.state.newtab ?
<Link to="/tabtest" hash="#scroll-tab-3" className="mdl-layout__tab">Tab 3</Link> : null}
</div>
</header>
<div className="mdl-layout__content">
<section className="mdl-layout__tab-panel is-active" id="scroll-tab-1">
<div className="page-content">Tab 1</div>
</section>
<section className="mdl-layout__tab-panel" id="scroll-tab-2">
<div className="page-content">Tab 2</div>
</section>
{ this.state.newtab ? <section className="mdl-layout__tab-panel" id="scroll-tab-3">
<div className="page-content">Tab 2</div>
</section> : null}
</div>
</div>);
}
I did some tests and could reproduce the problem. What helped was to put the key attribute on the root element of the tab component. When adding a new tab this key must change and react will throw away the component and rerender completly. This way all material-design-lite properties get lost and after calling upgradeDom or upgradeElement it works.
React and Material-Design-Lite
Material-Design-Lite source
From https://facebook.github.io/react/docs/component-api.html.
forceUpdate
void forceUpdate(
[function callback]
)
By default, when your component's state or props change, your component will re-render. However, if these change implicitly (eg: data deep within an object changes without changing the object itself) or if your render() method depends on some other data, you can tell React that it needs to re-run render() by calling forceUpdate().
Calling forceUpdate() will cause render() to be called on the component, skipping shouldComponentUpdate(). This will trigger the normal lifecycle methods for child components, including the shouldComponentUpdate() method of each child. React will still only update the DOM if the markup changes.
Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render(). This makes your component "pure" and your application much simpler and more efficient.
In order to highlight the most important part of Christian Steinmann's answer:
Give the surrounding .mdl-js-tabs div element the tabs count as the key attribute.
Whenever a tab is added, the key will change and React will rerender the whole tabs component. This will then the MDL upgrade.