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!
Related
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>
I can't figure out how to make card text editable and reflect that changes in the card state.
So, I have an input field where I type in text and click on add button which makes a call for add function,
addFunction = () => {
this.cardId ++;
this.setState({
t_c: [
...this.state.t_c,
{
cardId: this.cardId,
terms: this.state.terms
}
]
});
}
So terms are the text and its provided with cardId as id.
So to display the data inside state t_c I am mapping each object inside the state and displaying them,
{this.state.t_c.map(terms =>
<section>
<section>
<input defaultValue={terms.terms} />
</section>
<img ..../>
</section>
)}
The problem here if I try to change the added terms the changes are not displayed in the t_c state.
At first, you should approach your problem like it's a Form.
Form components are whether controlled or uncontrolled. For your scenario, if you want to save every change you have done to state, you should implement your input as controlled.
<section>
<input
value={terms.terms}
onChange={(e) => setTerms(/* your state controller code */)}
/>
</section>
Also to see the changes, you are missing key prop to populated sections. Change mapping function to this and you'll see the changes.
{this.state.t_c.map((terms, t_index)=>
<section key={"terms" + t_index}>
<section>
<input defaultValue={terms.terms} />
</section>
<img ..../>
</section>
)}
See more detail here.
This is kind of an example scenario what the problem looks like,
<div x-data="{ count : 0 }">
<div x-data>
<span x-text="count"></span>
<button x-on:click="count++">Increment</button>
<button x-on:click="count--">Decrement</button>
</div>
</div>
It would be able to increase/decrease the data count from the child component.
I thought of handling it through dispatching custom events using $dispatch() but then again in terms of design, I might need to write listeners on both parent and child component which make the logic more complex since it should be reactive as well.
There was a Github issue, and none of the proposed solutions was working.
I thought of handling it through dispatching custom events using $dispatch() but then again in terms of design, I might need to write listeners on both parent and child component which make the logic more complex since it should be reactive as well.
This is the crux of the issue, in order to do parent-child and child-parent communication you'll need to use events. In the case of child -> parent, you'll trigger increment and decrement events (which will be listened to in the parent component using x-on:increment and x-on:decrement). In the case of parent -> child, you'll need to use $watch to trigger updates whenever count updates (I'll used the new-count event name), this will be listened to on the window from the child component using x-on:new-count.window.
Here's the full working solution (see it as a CodePen):
<div
x-data="{ count : 0 }"
x-init="$watch('count', val => $dispatch('new-count', val))"
x-on:increment="count++"
x-on:decrement="count--"
>
<div>In root component: <span x-text="count"></span></div>
<div
x-data="{ count: 0 }"
x-on:new-count.window="count = $event.detail"
>
<div>In nested component <span x-text="count"></span></div>
<button x-on:click="$dispatch('increment')">Increment</button>
<button x-on:click="$dispatch('decrement')">Decrement</button>
</div>
</div>
In the case you've presented, the count state might be better served by using a global store that integrates with Alpine.js such as Spruce, in which case we'll read and update a shared global store to which both the parent and child components are subscribed (see the Spruce docs). You can find the working example in the following CodePen.
<div x-data x-subscribe="count">
<div>In root component: <span x-text="$store.count"></span></div>
<div x-data x-subscribe="count">
<div>In nested component <span x-text="$store.count"></span></div>
<button x-on:click="$store.count ++">Increment</button>
<button x-on:click="$store.count--">Decrement</button>
</div>
</div>
<script>
Spruce.store('count', 0);
</script>
The final solution that should be mentioned is that removing the nested component would mean that the count increment and decrement would work as expected. Obviously this example was probably simplified & meant to be illustrative, so this solution might not work in a lot of cases. Note: the only difference is removing the second x-data.
<div x-data="{ count : 0 }">
<div>
<span x-text="count"></span>
<button x-on:click="count++">Increment</button>
<button x-on:click="count--">Decrement</button>
</div>
</div>
Another way is to use the magic $store 👉as a Global State:
<div x-data x-init={Alpine.store('count', 0)}>
<div x-data>
<span x-text="$store.count"></span>
<button x-on:click="$store.count++">Increment</button>
<button x-on:click="$store.count--">Decrement</button>
</div>
</div>
I am creating a program for my Universities Research department, the program is for the operation of an Atomic Layer Deposition system.
The program needs to be able to allow the user to set a custom 'recipe' if you will, each grid-item will have a button with 'edit' in the bottom corner, and will take you to a separate page where you can enter your system stats like zone temps, durations, cycles, etc. We have gotten the draggable grid-items working, so that we could make the system run multiple steps over and over if we wish, but here is my goal I can't seem to accomplish:
We need to be able to add a new 'step' grid-item on button click, and we need to be able to remove a grid-item as well. I have created a button 'New Tile' and I defined a function I was hoping would work, I was trying to see if I could duplicate a grid item, but when I run the program and click the button I get the error:
"[Vue warn]: Property or method "duplicate" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties."
Image of app: https://imgur.com/a/DgKebPR
image of JS trying to be used: https://imgur.com/a/EjW4t6d
<h2 style="color: #f6a821;">Steps</h2>
<hr class="hr" />
<grid-layout
:layout.sync="stepsGrid"
:col-num="8"
:row-height="75"
:is-draggable="true"
:is-resizable="false"
:is-mirrored="false"
:vertical-compact="true"
:margin="[50, 50]"
:use-css-transforms="true">
<div id="duplicater">
<grid-item
:x=stepsGrid[0].x
:y=stepsGrid[0].y
:w=stepsGrid[0].w
:h=stepsGrid[0].h
:i=stepsGrid[0].i
:isDraggable=stepsGrid[0].isDraggable>
<div class="Panel__name">1</div>
<div class="editButton">
<router-link to="/Parameters-template" class="editButton">Edit</router-link></router-link>
</div><br>
<div class="Panel__status">Status:</div>
</grid-item>
<div id="bottombuttons">
<button id="resetbutton">Reset</button>
<button id="startbutton" #click="duplicate()">New Tile</button>
</div>
</div>
I don't get where is declared the duplicate method extactly. Don't expect it to be available from a template if it's not declared in the component's methods. Try:
methods: {
duplicate() {
/* */
}
}
in the same component. If it needs to be a shared among several components, put this in a mixin.
I'm relevantly new to React and I am having trouble on how to tackle this logic:
Essentially I am creating a table using flexbox, and I also want it so that when you click on one of the rows, it expands and reveals another row (for example, it will give a small description what it is about).
So far what I have is just the table.
class Application extends React.Component {
render() {
const renderDataRows = (
[
<div key={0} className='row'>
<div className='cell description'> Mortage Bill
</div>
<div className='cell amount'>$0,000,000</div>
<div className='cell amount'>$2.50</div>
<div className='cell amount'v>000%</div>
</div>,
<div key={1} className='row'>
<div className='cell description'> Electric Bill
</div>
<div className='cell amount'>$0,000,000</div>
<div className='cell amount'>$2.50</div>
<div className='cell amount'v>000%</div>
</div>,
]
)
const containerTable = (
<div className='table-container'>
{renderDataRows}
</div>
)
return (
<div>
{containerTable}
</div>
)
}
}
More specifically, what would be the best way to structure the hidden rows? Create as a child of the cells, or siblings?
I am assuming I will need state to keep in track what is current open, etc?
I've attached Codepen link to mess around
This can be done in the following way:
Let all the cells be in a single parent div and let the cell description be another sibling div (although using would be better). Put a class on the sibling div such as hidden. Not add a click handler on the cells div. Whenever this div is clicked, update the state with that div's id/key. Now use this to set the hidden class to the other divs. Compare this.state.key with the current id/key and show or hide accordingly. I am not giving the specific code.
Note: Instead of storing the divs in the renderDataRows, just put the data in it and map over it to create all the divs. That way you can easily manipulate the hidden class and any other variation in a single place without having to update it separately for each row of data.