I have two components. If I hovered over one I'd like to move (by changing style proporties) the other one component.
How can I achieve that?
In pure js it's simply
let elem1 = document.querySelector('.elem1');
let elem2 = document.querySelector('.elem2');
elemt1.addEventListener('mouseover', () => {
elem2.style.right = "200px" //or any other style property
})
So.. in react we can use "ref" and this works if I define static ref
import React, {Component} from 'react';
class MainCanvas extends Component {
onHover(){
console.log(this.refs.mybtntest);
}
render(){
return(
<div>
<h1 onMouseEnter={() => this.onHover()}> Testing</h1>
<button ref="mybtntest">Close</button>
</div>
);
}
}
export default MainCanvas
However in my case I need to each component should has dynamically added "ref" atribute.. so my code looks like below
import React, {Component} from 'react';
class Test extends Component {
onHover(e, dynamicRef){
console.log(dynamicRef); //correct number of ref
console.log(this.refs.dynamicRef); //undefined
console.log(this.refBtnName); //button reference but eachtime is overrided
console.log(this.dynamicRef);//undefinded
}
render(){
const elements = this.props.elements.map( element => {
let refBtnName = element.id + "btn";
return [
<ComponentElement
onHover={(e) => this.onHover(e, refBtnName)}
key={element.id} {...element}
/>,
<button key={element.id*2}
ref={refBtnName => this.refBtnName = refBtnName} //each time he will be overrided :(
className={`${refBtnName} deleteComponentBtn`} >
Close
</button>
]
})
return(
<div>
{elements}
</div>
);
}
}
export default Test
A real goal is that I want to positioning the button relative to the element. I could use div for this purpose as a wrapper but I don't want it.
So I thought to use for example this piece of code
onHover(e, dynamicRef){
Math.trunc(e.target.getBoundingClientRect().right)
dynamicRef.style.right = `${right}px`;
}
If you need dynamic object keys you shouldn't use the dot . and instead use the brackets:
ref={ref=> this[refBtnName] = ref}
Note that i changed the inline parameter to ref instead of refBtnName so you won't get variable names conflicts.
Running example:
class App extends React.Component {
state = {
items: [
{ name: 'John', id: 1 },
{ name: 'Mike', id: 2 },
{ name: 'Jane', id: 3 },
]
}
move = refName => e => {
this[refName].style.right = '-90px';
}
render() {
const { items } = this.state;
return (
<div>
{
items.map(item => {
return (
<div key={item.id} >
<div
ref={ref => { this[item.name] = ref }}
style={{ position: 'relative' }}
>
{item.name}
</div>
<button onClick={this.move(item.name)}>Move {item.name}</button>
</div>
)
})
}
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Related
I am trying to figure out how to toggle an active class on click to change CSS properties.
My code is below. Can anyone advise how I should do this? Without creating a new component for each item is it possible to do this?
class Test extends Component(){
constructor(props) {
super(props);
this.addActiveClass= this.addActiveClass.bind(this);
}
addActiveClass() {
//not sure what to do here
}
render() {
<div>
<div onClick={this.addActiveClass}>
<p>1</p>
</div>
<div onClick={this.addActiveClass}>
<p>2</p>
</div>
<div onClick={this.addActiveClass}>
<p>3</p>
</div>
</div>
}
}
Use state. See the React docs.
class MyComponent extends Component {
constructor(props) {
super(props);
this.addActiveClass= this.addActiveClass.bind(this);
this.state = {
active: false,
};
}
toggleClass() {
const currentState = this.state.active;
this.setState({ active: !currentState });
};
render() {
return (
<div
className={this.state.active ? 'your_className': null}
onClick={this.toggleClass}
>
<p>{this.props.text}</p>
</div>
)
}
}
class Test extends Component {
render() {
return (
<div>
<MyComponent text={'1'} />
<MyComponent text={'2'} />
</div>
);
}
}
You can also do this with hooks.
function MyComponent (props) {
const [isActive, setActive] = useState(false);
const toggleClass = () => {
setActive(!isActive);
};
return (
<div
className={isActive ? 'your_className': null}
onClick={toggleClass}
>
<p>{props.text}</p>
</div>
);
}
I would prefer using the && operator in an inline if statement. In my opinion it gives cleaner codebase this way.
Generally you could be doing something like this:
render(){
return(
<div>
<button className={this.state.active && 'active'}
onClick={ () => this.setState({active: !this.state.active}) }>Click me</button>
</div>
)
}
Just keep in mind that arrow functions are and ES6 feature and remember to set this.state.active value in the class constructor.
this.state = { active: false }
Or if you want to inject CSS in JSX you are able to do it this way:
<button style={this.state.active && style.button} >button</button>
And you can declare style json variable:
const style = { button: { background:'red' } }
Remember to use camelCase on JSX stylesheets.
Well, your addActiveClass needs to know what was clicked. Something like this could work (notice that I've added the information which divs are active as a state array, and that onClick now passes the information what was clicked as a parameter after which the state is accordingly updated - there are certainly smarter ways to do it, but you get the idea).
class Test extends Component(){
constructor(props) {
super(props);
this.state = {activeClasses: [false, false, false]};
this.addActiveClass= this.addActiveClass.bind(this);
}
addActiveClass(index) {
const activeClasses = [...this.state.activeClasses.slice(0, index), !this.state.activeClasses[index], this.state.activeClasses.slice(index + 1)].flat();
this.setState({activeClasses});
}
render() {
const activeClasses = this.state.activeClasses.slice();
return (
<div>
<div className={activeClasses[0]? "active" : "inactive"} onClick={() => this.addActiveClass(0)}>
<p>0</p>
</div>
<div className={activeClasses[1]? "active" : "inactive"} onClick={() => this.addActiveClass(1)}>
<p>1</p>
</div>
<div onClick={() => this.addActiveClass(2)}>
<p>2</p>
</div>
</div>
);
}
}
You can simply access the element classList which received the click event using event.target then by using toggle method on the classList object to add or remove the intended class
<div onClick={({target}) => target.classList.toggle('active')}>
....
....
....
</div>
Equevelent
<div onClick={e=> e.target.classList.toggle('active')}>
....
....
....
</div>
OR by declaring a function that handle the click and does extra work
function handleClick(el){
.... Do more stuff
el.classList.toggle('active');
}
<div onClick={({target})=> handleClick(target)}>
....
....
....
</div>
React has a concept of components state, so if you want to switch it, do a setState:
constructor(props) {
super(props);
this.addActiveClass= this.addActiveClass.bind(this);
this.state = {
isActive: false
}
}
addActiveClass() {
this.setState({
isActive: true
})
}
In your component use this.state.isActive to render what you need.
This gets more complicated when you want to set state in component#1 and use it in component#2. Just dig more into react unidirectional data flow and possibly redux that will help you handle it.
using React you can add toggle class to any id/element, try
style.css
.hide-text{
display: none !important;
/* transition: 2s all ease-in 0.9s; */
}
.left-menu-main-link{
transition: all ease-in 0.4s;
}
.leftbar-open{
width: 240px;
min-width: 240px;
/* transition: all ease-in 0.4s; */
}
.leftbar-close{
width: 88px;
min-width:88px;
transition: all ease-in 0.4s;
}
fileName.js
......
ToggleMenu=()=>{
this.setState({
isActive: !this.state.isActive
})
console.log(this.state.isActive)
}
render() {
return (
<div className={this.state.isActive===true ? "left-panel leftbar-open" : "left-panel leftbar-close"} id="leftPanel">
<div className="top-logo-container" onClick={this.ToggleMenu}>
<span className={this.state.isActive===true ? "left-menu-main-link hide-from-menu" : "hide-text"}>Welcome!</span>
</div>
<div className="welcome-member">
<span className={this.state.isActive===true ? "left-menu-main-link hide-from-menu" : "hide-text"}>Welcome<br/>SDO Rizwan</span>
</div>
)
}
......
The above answers will work, but just in case you want a different approach, try classname: https://github.com/JedWatson/classnames
A good sample would help to understand things better:
HTML
<div id="root">
</div>
CSS
.box {
display: block;
width: 200px;
height: 200px;
background-color: gray;
color: white;
text-align: center;
vertical-align: middle;
cursor: pointer;
}
.box.green {
background-color: green;
}
React code
class App extends React.Component {
constructor(props) {
super(props);
this.state = {addClass: false}
}
toggle() {
this.setState({addClass: !this.state.addClass});
}
render() {
let boxClass = ["box"];
if(this.state.addClass) {
boxClass.push('green');
}
return(
<div className={boxClass.join(' ')} onClick={this.toggle.bind(this)}>{this.state.addClass ? "Remove a class" : "Add a class (click the box)"}<br />Read the tutorial here.</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
you can add toggle class or toggle state on click
class Test extends Component(){
state={
active:false,
}
toggleClass() {
console.log(this.state.active)
this.setState=({
active:true,
})
}
render() {
<div>
<div onClick={this.toggleClass.bind(this)}>
<p>1</p>
</div>
</div>
}
}
Thanks to #cssko for providing the correct answer, but if you tried it yourself you will realise it does not work. A suggestion has been made by #Matei Radu, but was rejected by #cssko, so the code remains unrunnable (it will throw error 'Cannot read property bind of undefined'). Below is the working correct answer:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.addActiveClass = this.addActiveClass.bind(this);
this.state = {
active: false,
};
}
addActiveClass() {
const currentState = this.state.active;
this.setState({
active: !currentState
});
};
render() {
return ( <
div className = {
this.state.active ? 'your_className' : null
}
onClick = {
this.addActiveClass
} >
<
p > {
this.props.text
} < /p> < /
div >
)
}
}
class Test extends React.Component {
render() {
return ( <
div >
<
MyComponent text = {
'Clicking this will toggle the opacity through css class'
}
/> < /
div >
);
}
}
ReactDOM.render( <
Test / > ,
document.body
);
.your_className {
opacity: 0.3
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>
React has a concept of components state, so if you want to Toggle, use setState:
App.js
import React from 'react';
import TestState from './components/TestState';
class App extends React.Component {
render() {
return (
<div className="App">
<h1>React State Example</h1>
<TestState/>
</div>
);
}
}
export default App;
components/TestState.js
import React from 'react';
class TestState extends React.Component
{
constructor()
{
super();
this.state = {
message: 'Please subscribe',
status: "Subscribe"
}
}
changeMessage()
{
if (this.state.status === 'Subscribe')
{
this.setState({message : 'Thank You For Scubscribing.', status: 'Unsubscribe'})
}
else
{
this.setState({ message: 'Please subscribe', status: 'Subscribe' })
}
}
render()
{
return (
<div>
<h1>{this.state.message}</h1>
<button onClick={()=> this.changeMessage() } >{this.state.status}</button>
</div>
)
}
}
export default TestState;
Output
I started learning React recently and wanted to build a tab just to see how far my knowledge has gone. I came across this and decided to implement something without redux. I kind of feel the answers don't reflect what op wants to achieve. He wants only one active component but the answers here will set all components active. I have given it a shot.
Below is a tab file
import React, { Component } from 'react';
class Tab extends Component {
render(){
const tabClassName = "col-xs-3 tab-bar";
const activeTab = this.props.activeKey === this.props.keyNumber ? "active-tab" : null;
return (
<div
className = {`${tabClassName} ${activeTab}`}
onClick={()=>this.props.onClick(this.props.keyNumber)}
>
I am here
</div>
);
}
}
export default Tab;
The tabs file...
import React, { Component } from 'react';
import Tab from './tab';
class Tabs extends Component {
constructor(props){
super(props);
this.state = {
currentActiveKey: 0,
tabNumber: 2
};
this.setActive = this.setActive.bind(this);
this.setTabNumber = this.setTabNumber.bind(this);
}
setTabNumber(number){
this.setState({
tabNumber: number
});
}
setActive (key){
this.setState({
currentActiveKey: key
});
}
render(){
let tabs = [];
for(let i = 0; i <= this.state.tabNumber; i++){
let tab = <Tab key={i} keyNumber={i} onClick={this.setActive} activeKey={this.state.currentActiveKey}/>;
tabs.push(tab);
}
return (
<div className="row">
{tabs}
</div>
);
}
}
export default Tabs;
your index file...
import React from 'react';
import ReactDOM from 'react-dom';
import Tabs from './components/tabs';
ReactDOM.render(
<Tabs />
, document.querySelector('.container'));
and the css
.tab-bar {
margin: 10px 10px;
border: 1px solid grey;
}
.active-tab {
border-top: 1px solid red;
}
This is a skeleton of something I want to improve on so increasing the tabNumber beyond 4 will break the css.
Here is a code I came Up with:
import React, {Component} from "react";
import './header.css'
export default class Header extends Component{
state = {
active : false
};
toggleMenuSwitch = () => {
this.setState((state)=>{
return{
active: !state.active
}
})
};
render() {
//destructuring
const {active} = this.state;
let className = 'toggle__sidebar';
if(active){
className += ' active';
}
return(
<header className="header">
<div className="header__wrapper">
<div className="header__cell header__cell--logo opened">
<a href="#" className="logo">
<img src="https://www.nrgcrm.olezzek.id.lv/images/logo.svg" alt=""/>
</a>
<a href="#" className={className}
onClick={ this.toggleMenuSwitch }
data-toggle="sidebar">
<i></i>
</a>
</div>
<div className="header__cell">
</div>
</div>
</header>
);
};
};
Just wanted to add my approach. Using hooks and context provider.
Nav.js
function NavBar() {
const filterDispatch = useDispatchFilter()
const {filter} = useStateFilter()
const activeRef = useRef(null)
const completeRef = useRef(null)
const cancelRef = useRef(null)
useEffect(() => {
let activeClass = '';
let completeClass = '';
let cancelClass = '';
if(filter === ACTIVE_ORDERS){
activeClass='is-active'
}else if ( filter === COMPLETE_ORDERS ){
completeClass='is-active'
}else if(filter === CANCEL_ORDERS ) {
cancelClass='is-active'
}
activeRef.current.className = activeClass
completeRef.current.className = completeClass
cancelRef.current.className = cancelClass
}, [filter])
return (
<div className="tabs is-centered">
<ul>
<li ref={activeRef}>
<button
className="button-base"
onClick={() => filterDispatch({type: 'FILTER_ACTIVE'})}
>
Active
</button>
</li>
<li ref={completeRef}>
<button
className="button-base"
onClick={() => filterDispatch({type: 'FILTER_COMPLETE'})}
>
Complete
</button>
</li>
<li ref={cancelRef}>
<button
className={'button-base'}
onClick={() => filterDispatch({type: 'FILTER_CANCEL'})}
>
Cancel
</button>
</li>
</ul>
</div>
)
}
export default NavBar
filterContext.js
export const ACTIVE_ORDERS = [
"pending",
"assigned",
"pickup",
"warning",
"arrived",
]
export const COMPLETE_ORDERS = ["complete"]
export const CANCEL_ORDERS = ["cancel"]
const FilterStateContext = createContext()
const FilterDispatchContext = createContext()
export const FilterProvider = ({ children }) => {
const [state, dispatch] = useReducer(FilterReducer, { filter: ACTIVE_ORDERS })
return (
<FilterStateContext.Provider value={state}>
<FilterDispatchContext.Provider value={dispatch}>
{children}
</FilterDispatchContext.Provider>
</FilterStateContext.Provider>
)
}
export const useStateFilter = () => {
const context = useContext(FilterStateContext)
if (context === undefined) {
throw new Error("place useStateMap within FilterProvider")
}
return context
}
export const useDispatchFilter = () => {
const context = useContext(FilterDispatchContext)
if (context === undefined) {
throw new Error("place useDispatchMap within FilterProvider")
}
return context
}
export const FilterReducer = (state, action) => {
switch (action.type) {
case "FILTER_ACTIVE":
return {
...state,
filter: ACTIVE_ORDERS,
}
case "FILTER_COMPLETE":
return {
...state,
filter: COMPLETE_ORDERS,
}
case "FILTER_CANCEL":
return {
...state,
filter: CANCEL_ORDERS,
}
}
return state
}
Works fast, and replaces redux.
const aDiv = useRef(null);
function app(){
const [isDark, setIsDark] = useState();
useEffect(()=>{
if(isDark){
aDiv.current.classList.add('dark-mode')
}else{
aDiv.current.classList.remove('dark-mode')
}
},[isDark]}
return <div className = "app" ref = {aDiv}> </div>
useRef to id the element to toggle the class, then a boolean useState to track switching, on true, we get the ref's current classList then add a className else we remove the className.
All this happen in the useEffect with our useState as dependency array.
import { useState } from "react";
import "./App.css";
export default function App() {
const [isActive, setIsActive] = useState(false);
const handleClick = (event) => {
// ️ toggle isActive state on click
setIsActive((current) => !current);
};
return (
<div>
<button className={isActive ? "bg-salmon" : ""} onClick={handleClick}>
Click
</button>
</div>
);
}
I'm trying to change the class of an element using the onClick event handler. When the div is clicked the class changes causing some css to change in turn. When I add another div, it assumes the same state as the first div.
class Content extends React.Component {
constructor(props) {
super(props);
this.startGambling = this.startGambling.bind(this);
this.toggleClass = this.toggleClass.bind(this);
this.state = {
prize: '',
tries: 0,
isFlipped: false
};
}
toggleClass() {
this.setState({ isFlipped: true });
};
render() {
return (
<div className="card-box" style={divStyle}>
<div class="flip-card-inner" className={this.state.isFlipped ? 'flipped' : null} onClick={this.toggleClass}></div>
</div>
);
}
}
State is not html-element-scoped. It is the state of the current Component.
Implement the FlipCard component and handle its flipped state within that component.
class FlipCard extends React.Component {
state = {
isFlipped: false
}
toggle = () => {
this.setState(prevState => ({
isFlipped: !prevState.isFlipped
}))
}
render(){
const { isFlipped } = this.state;
return (
<div
onClick={this.toggle}
className={isFlipped ? 'flipped' : ''}>
Card
</div>
)
}
}
const CardBox = () => (
<article>
<FlipCard />
<FlipCard />
</article>
);
ReactDOM.render(<CardBox />, document.getElementById('root'))
div {
cursor: pointer;
}
div.flipped {
background: red;
}
<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"></div>
This is a different approach than #gazdagergo's answer but logic is the same. You need to track toggled state somewhere. If you don't need this toggled state anywhere else, you can keep it in its own components as #gazdagergo showed. But, for example, if you need to know how many items are toggled (just an example) you can keep the state in the parent component to share this info with another component.
const cards = [
{ id: 1, name: "foo" },
{ id: 2, name: "bar" },
{ id: 3, name: "baz" }
];
const Card = ({ card, toggleClass, isFlipped }) => {
const handleClick = () => toggleClass(card.id);
return (
<div onClick={handleClick} className={isFlipped[card.id] ? "flipped" : ""}>
{card.name}
</div>
);
};
class Content extends React.Component {
constructor(props) {
super(props);
this.toggleClass = this.toggleClass.bind(this);
this.state = {
prize: "",
tries: 0,
isFlipped: {}
};
}
toggleClass(id) {
this.setState(state => ({
isFlipped: { ...state.isFlipped, [id]: !state.isFlipped[id] }
}));
}
render() {
return (
<div>
{cards.map(card => (
<Card
key={card.id}
card={card}
toggleClass={this.toggleClass}
isFlipped={this.state.isFlipped}
/>
))}
</div>
);
}
}
ReactDOM.render(<Content />, document.getElementById("root"));
.flipped {
background: red;
}
<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" />
In the example, I assumed that card items have a unique id but you can do that with indexes also. Your isFlipped state is not a boolean anymore, it is an object and keeps the flipped ids.
I have this data in my redux store which I want to render in my react component .
{
"entityMap":{
"0":{
"type":"LINK",
"mutability":"MUTABLE",
"data":{"url":"www.google.co.in"}
}
},
"blocks":[
{
"key":"9k5h7",
"text":"this is the link",
"type":"unstyled",
"depth":0,
"inlineStyleRanges":[],
"entityRanges":[
{
"offset":12,
"length":4,
"key":0
}
],
"data":{}
}
]
}
I succesfully managed to create a link type with draft editor and was able to store it in database and while rendering it I am getting the entire text except for the link . I have this link information in my redux i.e the "entity map" and also the "entityRanges" inside "blocks" which tells on which offset the link starts and what is the length . for eg, here in my case it is "link" in "this is the link" .
Here is the code which i used to render the above json from my redux :
render(){
return(
<div>
{
var nn = abovejsonfromreduxstore;
var editorState = EditorState.createWithContent(convertFromRaw(JSON.parse(nn)));
return (
<div>
<pre>
<Editor
editorState={editorState}
readOnly
/>
</pre>
</div>
</div>
}
</div>
);
}
How to modify this rendering method so that it renders the link entity too ?
You should specify draft.js decorator this way:
const decorator = new CompositeDecorator([
{
strategy: findLinkEntities,
component: Link,
},
]);
Pass the findLinkEntities function to strategy property and Link react component to component property:
function findLinkEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
);
},
callback
);
}
const Link = (props) => {
const {url} = props.contentState.getEntity(props.entityKey).getData();
return (
<a href={url}>
{props.children}
</a>
);
};
After that pass this decorator to createWithContent method:
this.state = {
editorState: EditorState.createWithContent(convertFromRaw(initialStateRaw), decorator)
};
Check working example in the hidden snippet below:
const {Editor, CompositeDecorator, convertFromRaw, EditorState} = Draft;
const initialStateRaw = {
"entityMap":{
"0":{
"type":"LINK",
"mutability":"MUTABLE",
"data":{"url":"www.google.co.in"}
}
},
"blocks":[
{
"key":"9k5h7",
"text":"this is the link",
"type":"unstyled",
"depth":0,
"inlineStyleRanges":[],
"entityRanges":[
{
"offset":12,
"length":4,
"key":0
}
],
"data":{}
}
]
};
function findLinkEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
);
},
callback
);
}
const Link = (props) => {
const {url} = props.contentState.getEntity(props.entityKey).getData();
return (
<a href={url}>
{props.children}
</a>
);
};
class Container extends React.Component {
constructor(props) {
super(props);
const decorator = new CompositeDecorator([
{
strategy: findLinkEntities,
component: Link,
},
]);
this.state = {
editorState: EditorState.createWithContent(convertFromRaw(initialStateRaw), decorator)
};
}
_handleChange = (editorState) => {
this.setState({ editorState });
}
render() {
return (
<div className="container-root">
<Editor
placeholder="Type away :)"
editorState={this.state.editorState}
onChange={this._handleChange}
/>
</div>
);
}
}
ReactDOM.render(<Container />, document.getElementById('react-root'))
body {
font-family: Helvetica, sans-serif;
}
.container-root {
border: 1px solid black;
padding: 5px;
margin: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/draft-js/0.7.0/Draft.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/draft-js/0.10.0/Draft.js"></script>
<div id="react-root"></div>
I'm ReactJS newbie (well, to be specific I'm programming newbie) looking for some answers, please don't exclude me for obvious stupidity :) I'm having big troubles with passing props and understanding 'this' context.
This is my mess, two simple Todo app components. TodoApp:
import React from 'react';
import uuid from 'uuid';
import style from './App.css';
import Title from '../components/Title.js';
import TodoList from '../components/TodoList.js';
class App extends React.Component {
constructor(props) {
super(props);
this.title = "Todo Application"
this.state = {
data: [{
id: 1,
text: 'clean room'
}, {
id: 2,
text: 'wash the dishes'
}, {
id: 3,
text: 'feed my cat'
}]
};
}
removeTodo(id) {
const remainder = this.state.data.filter(item => item.id !== id);
this.setState({data: remainder});
}
render() {
return (
<div className={style.TodoApp}>
<Title title="Todo Application" added={this.state.data.length} />
<TodoList data={this.state.data} remove={this.removeTodo}></TodoList>
</div>
);
}
}
export default App;
And TodoList:
import React from 'react';
const TodoList = props => (
<ul>
{props.data.map((item, index) =>
<li key={index}>
{item.text}
<button value={index} onClick={() => props.remove(index)}>x</button>
</li>
)}
</ul>
);
export default TodoList;
My question is how to correctly pass props to child component (TodoList) so that remove button would work?
To make sure the this always refers to the App context where your removeTodo method is defined, you can add the following inside your constructor (after setting the initial state):
this.removeTodo = this.removeTodo.bind(this);
Otherwise, your code isn't messy at all. It's even surprisingly concise and well thought out, even much more so for a self-proclaimed beginner. Congratulations!
As pointed out by Sag1v in the comment below, you're also not passing the correct value to your removeTodo method. You're passing the index of the item being iterated on, instead of its id.
Change your <button> invocation to the following:
<button value={index} onClick={() => props.remove(item.id)}>x</button>
Note that you could also achieve the same with the following:
<button value={index} onClick={props.remove.bind(null, item.id)}>x</button>
You got 2 main problems here:
As Jaxx mentioned you are not binding the handler removeTodo to
the class.
There are couple of ways to do it.
Bind it in the constructor:
this.removeTodo = this.removeTodo.bind(this);
Or use the ES2015(ES6) Arrow functions which will use the lexical context for this:
removeTodo = (id) => {
const remainder = this.state.data.filter(item => item.id !== id);
this.setState({data: remainder});
}
The 2nd problem is that inside TodoList onClick handler you are
not passing the correct id to the handler, you are passing the
index position.
onClick={() => props.remove(index)}
You should change that to:
onClick={() => props.remove(item.id)}
There is another problem with this approach which i'll explain next.
Here is a working example:
const Title = ({title}) => <h1>{title}</h1>
const TodoList = props => (
<ul>
{props.data.map((item, index) =>
<li key={index}>
{item.text}
<button value={index} onClick={() => props.remove(item.id)}>x</button>
</li>
)}
</ul>
);
class App extends React.Component {
constructor(props) {
super(props);
this.title = "Todo Application"
this.state = {
data: [{
id: 1,
text: 'clean room'
}, {
id: 2,
text: 'wash the dishes'
}, {
id: 3,
text: 'feed my cat'
}]
};
}
removeTodo = (id) => {
const remainder = this.state.data.filter(item => item.id !== id);
this.setState({data: remainder});
}
render() {
return (
<div>
<Title title="Todo Application" added={this.state.data.length} />
<TodoList data={this.state.data} remove={this.removeTodo}></TodoList>
</div>
);
}
}
ReactDOM.render(<App/>,document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
As i said, there is a problem with this approach, you are passing a new instance of a function on each render with this line of code:
onClick={() => props.remove(item.id)
This is considered as bad practice because this can interrupt the Reconciliation and diffing algorithm
But as we know, event Handlers should get a function reference, hence you can't just pass a function invocation like this
onClick={props.remove(item.id)}
This will pass the function's return type (if any) and not the reference for the function.
So the proper way of passing a function reference is like this:
onClick={props.remove}
But that is not good for your case as you need to pass back to the parent the current item id, but i'm afraid that the browser will only pass back the event parameter.
So what are the alternatives you ask?
Create another component and control the data you pass in and out from your component instead of relying on the goodwill of the browsers.
Here is another working example but this time without creating a new function instance on each render
const Title = ({title}) => <h1>{title}</h1>
class TodoItem extends React.Component {
handleClick = () => {
const{item, onClick} = this.props;
onClick(item.id);
}
render(){
const {item} = this.props;
return(
<li>
{item.text}
<button value={item.id} onClick={this.handleClick}>x</button>
</li>
);
}
}
const TodoList = props => (
<ul>
{props.data.map((item, index) =>
<TodoItem key={index} item={item} onClick={props.remove} />
)}
</ul>
);
class App extends React.Component {
constructor(props) {
super(props);
this.title = "Todo Application"
this.state = {
data: [{
id: 1,
text: 'clean room'
}, {
id: 2,
text: 'wash the dishes'
}, {
id: 3,
text: 'feed my cat'
}]
};
}
removeTodo = (id) => {
const remainder = this.state.data.filter(item => item.id !== id);
this.setState({data: remainder});
}
render() {
return (
<div>
<Title title="Todo Application" added={this.state.data.length} />
<TodoList data={this.state.data} remove={this.removeTodo}></TodoList>
</div>
);
}
}
ReactDOM.render(<App/>,document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Ok, so the problem is pretty simple, but hard to explain.
I'm making an InputGenerator component, which generates a list of inputs.
Each generated input has a corresponding "remove" button next to it. The two elements (the input and the button) are wrapped in a div inside a map function. The div has a unique key prop. It looks like this (this is the whole component's jsx):
<div style={[InputGenerator.style.wrapper]}>
<div className="container" style={InputGenerator.style.container}>
{this.state.inputs.map((input, idx) => {
return (
<div key={idx} style={[
InputGenerator.style.row,
InputGenerator.count > 1 && idx > 0 ? InputGenerator.style.input.pushTop : {},
]}>
<InputText
id={input.id}
name={input.name}
placeholder={input.placeholder}
style={input.style}
/>
<Button
style={InputGenerator.style.remove}
type={Button.TYPES.BASIC}
icon="ion-close-round"
onClick={this.remove.bind(this, input.id)}
/>
</div>
);
})}
</div>
<div className="controls" style={InputGenerator.style.controls}>
<Button icon="ion-plus" type={Button.TYPES.PRIMARY} title="Add ingredient" onClick={this.add.bind(this)}/>
</div>
</div>
As you may see, all the inputs are kept in the this.state object and each one is given an unique id.
Here are the are add and remove methods:
add():
add() {
InputGenerator.count++;
const newInput = {
id: this.id,
name: this.props.name,
placeholder: this.props.placeholder,
style: this.style,
value: '',
};
const inputs = this.state.inputs;
inputs.push(newInput);
this.setState({ inputs });
}
remove():
remove(id) {
this.setState({
inputs: this.state.inputs.filter(i => i.id !== id),
});
}
The problem is:
I generate three inputs (using the add button)
I put random values in the inputs (e.g: 1, 2, 3)
I click on the remove button, corresponding to the first element (with value 1)
Result: Two input items with values 1 and 2
Expected: Two input items with values 2 and 3
The problem: I suggest that the key prop on the wrapping div is not enough for react to keep track of the input's values.
So, I'm open for ideas and suggestions how to proceed.
Here's an isolated sandbox to play around with my component and see the "bug" in action: https://codesandbox.io/s/5985AKxRB
Thanks in advance! :)
The issue you facing is because you are not handling state properly. You need to update state when you change input value.
handleChange(index,event) {
let inputs = this.state.inputs;
inputs[index].value = event.target.value;
this.setState({inputs:inputs})
}
DEMO : DEMO
Here is the updated code:
import React, { Component } from 'react';
import { render } from 'react-dom';
import Hello from './Hello';
const styles = {
fontFamily: 'sans-serif',
textAlign: 'center',
};
const App = () =>
<div style={styles}>
<InputGenerator />
</div>;
class InputGenerator extends Component {
constructor() {
super();
this.state = {
inputs: [],
};
}
componentWillMount() {
this.add();
}
handleChange(index,event) {
let inputs = this.state.inputs;
inputs[index].value = event.target.value;
this.setState({inputs:inputs})
}
add() {
InputGenerator.count++;
const newInput = {
id: this.id,
name: this.props.name,
placeholder: this.props.placeholder,
style: this.style,
value: '',
};
const inputs = this.state.inputs;
inputs.push(newInput);
this.setState({ inputs });
}
get id() {
if (this.props.id) {
return `${this.props.id}-${InputGenerator.count}`;
}
return `InputGeneratorItem-${InputGenerator.count}`;
}
get style() {
return [];
}
remove(id) {
var state = this.state;
state.inputs = state.inputs.filter(i => i.id !== id);
this.setState({
inputs: state.inputs
});
}
render() {
return (
<div>
<div className="container">
{this.state.inputs.map((input, idx) => {
return (
<div key={idx}>
<input
id={input.id}
name={input.name}
value = {input.value}
onChange={this.handleChange.bind(this,idx)}
placeholder={input.placeholder}
/>
<button onClick={this.remove.bind(this, input.id)}>
Remove
</button>
</div>
);
})}
</div>
<div className="controls">
<button onClick={this.add.bind(this)}>Add</button>
</div>
</div>
);
}
}
InputGenerator.count = 0;
render(<App />, document.getElementById('root'));