React - passing ref to sibling - javascript

I need to have 2 sibling components, and 1 of them has to have a reference to another - is that possible?
I tried this:
<button ref="btn">ok</button>
<PopupComponent triggerButton={this.refs.btn} />
And even this
<button ref="btn">ok</button>
<PopupComponent getTriggerButton={() => this.refs.btn} />
But I get undefined in both situations. Any ideas?
Note: a parent component will not work for me, because I need to have multiple sets of those, like
<button />
<PopupComponent />
<button />
<PopupComponent />
<button />
<PopupComponent />
NOT like this:
<div>
<button />
<PopupComponent />
</div>
<div>
<button />
<PopupComponent />
</div>
<div>
<button />
<PopupComponent />
</div>

Think this question is best answered by the docs:
If you have not programmed several apps with React, your first
inclination is usually going to be to try to use refs to "make things
happen" in your app. If this is the case, take a moment and think more
critically about where state should be owned in the component
hierarchy. Often, it becomes clear that the proper place to "own" that
state is at a higher level in the hierarchy. Placing the state there
often eliminates any desire to use refs to "make things happen" –
instead, the data flow will usually accomplish your goal.
Not sure exactly what you are trying to do, but my hunch is a parent component and passing props is what you really want here.

I completely agree with the quote Mark McKelvy has provided. What you are trying to achieve is considered an anti-pattern in React.
I'll add that creating a parent component doesn't necessarily means it has to be a direct parent, you can create a parent component further up the chain, in which you can render an array of all your children components together, having the logic to coordinate between all the children (or pairs of children according to your example) sit inside your parent.
I created a rough example of the concept which should do the trick:
class A extends React.Component {
onClick(key) {
alert(this.refs[key].refs.main.innerText);
}
render() {
var children = [];
for (var i = 0; i < 5; i++)
children.push.apply(children, this.renderPair(i));
return (
<div>
{children}
</div>
);
}
renderPair(key) {
return [
<B ref={'B' + key} key={'B' + key} onClick={this.onClick.bind(this, 'C' + key)}/>,
<C ref={'C' + key} key={'C' + key} onClick={this.onClick.bind(this, 'B' + key)}/>
];
}
}
class B extends React.Component {
render() {
return <p ref="main" onClick={this.props.onClick}>B</p>;
}
}
class C extends React.Component {
render() {
return <p ref="main" onClick={this.props.onClick}>C</p>;
}
}
React.render(<A/>, document.getElementById('container'));
And any state you need to save for all your children, you do in the common parent. I really hope this helps.

The following code helps me to setup communication between two siblings. The setup is done in their parent during render() and componentDidMount() calls. Hope it helps.
class App extends React.Component<IAppProps, IAppState> {
private _navigationPanel: NavigationPanel;
private _mapPanel: MapPanel;
constructor() {
super();
this.state = {};
}
// `componentDidMount()` is called by ReactJS after `render()`
componentDidMount() {
// Pass _mapPanel to _navigationPanel
// It will allow _navigationPanel to call _mapPanel directly
this._navigationPanel.setMapPanel(this._mapPanel);
}
render() {
return (
<div id="appDiv" style={divStyle}>
// `ref=` helps to get reference to a child during rendering
<NavigationPanel ref={(child) => { this._navigationPanel = child; }} />
<MapPanel ref={(child) => { this._mapPanel = child; }} />
</div>
);
}
}

special-props
Special Props Warning
Most props on a JSX element are passed on to the component, however, there are two special props (ref and key) which are used by React, and are thus not forwarded to the component.
For instance, attempting to access this.props.key from a component (eg. the render function) is not defined. If you need to access the same value within the child component, you should pass it as a different prop (ex: ). While this may seem redundant, it's important to separate app logic from reconciling hints.

Related

Using Javascript to create html custom Tag

class Headers extends React.Component {
render() {
const selected = this.props.selectedPane;
const headers = this.props.panes.map((pane, index) => {
const title = pane.title;
const klass = index === selected ? 'active' : '';
return (
<li
key={index}
className={klass}
onClick={() => this.props.onTabChosen(index)}>
{title}{' '}
</li>
);
});
return (
<ul className='tab-header'>
{headers}
</ul>
);
}
}
export default class Tabs extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedPane: 0
};
this.selectTab = this.selectTab.bind(this);
}
selectTab(num) {
this.setState({selectedPane: num});
}
render() {
const pane = this.props.panes[this.state.selectedPane];
return (
<div>
<h1>Tabs</h1>
<div className='tabs'>
<Headers
selectedPane={this.state.selectedPane}
//onTabChosen={this.selectTab}
panes={this.props.panes}>
</Headers>
<div className='tab-content'>
<article>
hellooooo
{pane.content}
</article>
</div>
</div>
</div>
);
}
}
I'm currently creating a 3 tab section where if you click on a tab, it gives you a new pane.
When looking at the render function I see a custom tag called Headers.
I know it coming from the Headers class at the beginning, but how does that format work? Is that a custom tag we building?
Also when looking at its properties such as onTabChosen, when it is deleted in the render method (for learning purposes) and I click on a selected tab, an error comes up saying
"_this.props.onTabChosen is not a function".
this.props.onTabChosen(index).. was written in the Headers class but not as a function correct?
I guess because I am also confused on how this.props.onTabChosen(index) works since onTabChosen was never declared anywhere, just input after props.
When looking at the render function I see a custom tag called "Headers".
That is not a custom tag. That is a React Component.
I know it coming from the Headers class at the beginning, but how does that format work?
Headers is either a function or a class (i.e. a constructor function).
The function will be called and the first argument passed to it will be an object with properties and values that match the props on the JSX element.
If you're going to use React then read a tutorial, this is very introductory level stuff for the framework.
It is covered very early on in both the MDN tutorial and the official React tutorial.
I guess because I am also confused on how this.props.onTabChosen(index) works since onTabChosen was never declared anywhere, just input after props.
It was declared, just not in the piece of code you shared.

Creating and displaying new elements on event (React.JS)

I've used stack overflow for the last two years since I picked up programming, but I've never actaully asked a question directly. However, recently I've been running into a problem with React which has stumped me for the last week, and it feels like one of those problems a pro will look at and instantly know the answer to.
My code: https://codepen.io/gooeyWolf/pen/xxZGxGq
<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>
<body>
<div id="root"><div id="shit"></div></div>
<script src="https://unpkg.com/react#16.12.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.12.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/#babel/standalone#7.8.3/babel.js"></script>
<script type="text/babel">
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: '',};
this.handleChange = this.handleChange.bind(this);
}
NextThing(){
const element = <h1>Hello, world!</h1>;
const divy = document.body.getElementById("div2");
divy.appendChild(element);
//document.body.appendChild(element);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleEnter(event){
if(event.key === 'Enter'){
this.NextThing();
}
}
render() {
return (
<div>
<span>Are you writing for a protagonist, antagonist, love interest, or secondary character? <br /></span>
<input name="flip" id="lucky" value={this.state.value} onChange={this.handleChange} onKeyDown={this.handleEnter} />
<div id="div2"></div>
</div>
);
}
}
//onChange={this.handleChange}
ReactDOM.render(
<NameForm />,
document.getElementById('root')
);
</script>
</body>
The app Im building is pretty simple. Basically, the app asks you a question, you type in your answer, and it displays a new block of text and a new input box to type your next answer, while leaving previous text on the screen.
So far, when you type something and press enter, the programs handles the enter, and calls another function to display the next block of text.
At first, i tried using return/render statements, but this cause the page to refresh, which would be fine, but the new elements I wanted to display doesn't show up. The React.JS course I'm taking advises against using creatElement statements for every new element required, so I'm working with appendChild, which for some reason this runs into errors.
I feel like there has to be a more simple solution to this, and a more reusable solution too. Ideally, it would be able to handle a refresh and keep the previously added elements.
I wrote this program in python first, with print and input statements, and it was so easy, but I spent the last week trying figure out the right way with React, but all my solutions seem unnecessarily complicated. There has to be a better way...
Thank you guys so much, this helps more than I can express.
Stack Overflow the best (:
In React, the common DOM manipulation functions (appendChild, innerHtml etc) are not the "best" way to do this, there is a more usable solution as you said. What you can do, is take advantage of the way JavaSript expressions can be renderered inside the HTML (That is, those that are inside with {{variable}} with curly braces) and combine it with this.state.
First in this.state, define an array div2Children which will contain all the text that is typed into the input
// This is inside the constructor
this.state = {value: '', div2Children: []};
Then you can map through each element and convert them in HTML.
render() {
return (
<div>
<span>Are you writing for a protagonist, antagonist, love interest, or secondary character? <br /></span>
<input name="flip" id="lucky" value={this.state.value} onChange={this.handleChange} onKeyDown={this.handleEnter} />
<div id="div2">
{/* when the array changes this page will also change the HTML */}
{this.state.div2Children.map(child => (
<h1>{child}</h1>
))}
</div>
</div>
);
}
Then finally in your NextThing method you'll have to change with this.setState the div2Children array to have all the previous elements but also your newly typed (appending the last element but creating a new array)
NextThing(){
this.setState({div2Children : [...this.state.div2Children, this.state.value]});
}
Note that you will also have to bind NextThing and handleChange. Here is a working version
<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>
<body>
<div id="root"><div id="shit"></div></div>
<script src="https://unpkg.com/react#16.12.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.12.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/#babel/standalone#7.8.3/babel.js"></script>
<script type="text/babel">
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: '', div2Children: []};
// Bind all the functions
this.NextThing = this.NextThing.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleEnter = this.handleEnter.bind(this);
}
NextThing(){
this.setState({div2Children : [...this.state.div2Children, this.state.value]});
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleEnter(event){
if(event.key === 'Enter'){
this.NextThing();
}
}
render() {
return (
<div>
<span>Are you writing for a protagonist, antagonist, love interest, or secondary character? <br /></span>
<input name="flip" id="lucky" value={this.state.value} onChange={this.handleChange} onKeyDown={this.handleEnter} />
<div id="div2">
{this.state.div2Children.map((child, index) => (
<h1 key={index}>{child}</h1>
))}
</div>
</div>
);
}
}
//onChange={this.handleChange}
ReactDOM.render(
<NameForm />,
document.getElementById('root')
);
</script>
</body>
Also notice here that I am also setting a key attribute in the snippet above. You can find more about that here, I just provided a simple solution which should be fine.
Firstly, you can't access to the DOM elements like that in React. Check the refs react API. Anyway, I think you should reverse your reflection here and avoid those dom-manipulation (if you can).
The data should be enough to display questions and answers.
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {
step: 1,
values: [''],
questions: [
{
id: 1,
label: 'Are you writing for a protagonist, antagonist, love interest, or secondary character?',
},
{
id: 2,
label: 'Are you happy?',
}
// other questions
]};
}
NextThing = (next) => {
this.setState((prev) => ({step: prev.step+1, values: [...prev.values, '']}));
}
handleChange = (index, event) => {
event.persist()
this.setState(state => {
const values = state.values.map((item, j) => {
if (j === index) {
return event.target.value;
} else {
return item;
}
});
return {
values,
};
});
}
handleEnter = (index, event) => {
if(event.key === 'Enter' && this.state.step < this.state.questions.length){
this.NextThing(index);
}
}
render() {
const { questions, step, values } = this.state;
return (
<div>
{questions.slice(0, step).map((question, index) => (
<div key={question.id}>
<span>{question.label}<br /></span>
<input
name={"answer"+question.id}
id={"answer"+question.id}
value={values[index]}
onChange={e => this.handleChange(index, e)}
onKeyDown={e => this.handleEnter(index, e)} />
</div>
))}
</div>
);
}
}
ReactDOM.render(
<NameForm />,
document.getElementById('root')
);
Let me know if I misunderstood something or if it helps :D
As others wrote, you shouldn't try to manipulate DOM like regular javascript in React.
So; we shouldnt manipulate elements like regular js, but what we can do is we can change stuff inside HTML with Refs and States. You can use ref's to give an element some kind of "id" in React way, and change its properties. State is one of the key elements of React and its quite powerful.
https://reactjs.org/docs/state-and-lifecycle.html
We can toggle on and off divs, inputs and other elements visibility, innerTexts and etc.
https://reactjs.org/docs/hooks-state.html
I will use a State Hook to change displaying of a div in here:
const [testState, setTestState] = useState(false)
...
return (
<>
<h1>Example Page</h1>
{testState ? <input placeholder="Example Input" /> : null}
</>
)
If my testState is true; an input box will show up; and if its false nothing will. We can use setTestState(true) -or false- now somewhere in our code to change visibility rendered html block.
Now, not sure if i understood your problem and phyton code correctly (i probably didnt), i quickly made a demo with using state's to toggle an elements visibility.
https://codesandbox.io/s/zealous-taussig-0ctif

How can React add and remove work together?

I am new to react and am trying to toggle a body class using two different buttons. One is supposed to add a class using an onClick event and the other is supposed to remove the class. Below is an example of my code.
Right now in the console I can see the event fire twice but the class remains. As I stated I am new to React so I know I may be doing this incorrectly.
bodyFixed() {
document.body.classList.add('body-fixed');
}
bodyRelative() {
document.body.classList.remove('body-fixed');
}
You are trying to modify the dom directly like you would with vanilla js or JQuery, but this is not how react is meant to be used. React creates a virtual dom that you create and manage, and then react handle changing the page for you.
I recommend following a guide like this one to learn basic setup and concepts (skip to the part where he uses JSX).
I can further point you in the right direction if you show your whole component file.
You want to toggle a className prop value in the React way.
The React way is having a state prop and having a handler function that will toggle the state value, rather than manipulating the DOM node directly (the way you're doing it).
I would suggest you to take a look at React Main Concepts: Handling events and later once you feel a little bit more comfortable to read about Virtual DOM and Reconciliation in React.
Here's how can you do it:
const { classNames } = window
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
isToggled: true
}
this.toggleClass = this.toggleClass.bind(this)
}
toggleClass() {
const { isToggled } = this.state
this.setState({
isToggled: !isToggled
})
}
render() {
const { isToggled } = this.state
const className = classNames({
'body-fixed': isToggled
})
return <div className={className}>
<div>Current `className`: <b>{ className }</b></div>
<button onClick={this.toggleClass}>Toggle class</button>
</div>
}
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
<script src="https://unpkg.com/classnames#2.2.6/index.js"></script>
<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="container"></div>
I was able to use the code I listed earlier. I had my onClick events positioned incorrectly. Here is an example of the code I used:
bodyFixed() {
document.body.classList.add('body-fixed');
}
bodyRelative() {
document.body.classList.remove('body-fixed');
}
<Toggle>
{({on, getTogglerProps, setOn, setOff, toggle, showAlert}) =>
<div>
<button className="search-icon-top" {...getTogglerProps()}>{on ? <img className="times" onClick={this.bodyRelative} src={require('img/times.svg')} alt=" " /> : <i className="fa fa-search" onClick={this.bodyFixed}></i>}</button>
<div>
{on ? <TodaySearchBox /> : ''}
</div>
</div>}
</Toggle>
This is just a start for now. Thank you for the input.
EDIT: I am open to suggestions. Like I said I am new to React.

Why does my React grandchild component not update after receiving a change in props from its parent? Or: do I need to use state?

I have tried finding the answer to this on StackOverflow and there are some related posts (e.g. React Child Component Not Updating After Parent State Change) but I want to understand why this is not working...
I have a React application that will display a layout of character cards (that is, each card displays a different character). It uses a child component, CharacterBoard, that lays out the CharacterCards, which would be a grandchild component. I pass the characters down from the App to the CharacterBoard as props, and CharacterBoard in turn maps these out the CharacterCards.
The problem is that I want the state of the character to change when I click on one of them. Specifically, I want the revealed field to change. However, even though the state change is reflected in the array of characters in the App (that is, the revealed field changes correctly), and the change is reflected in the array of characters in CharacterBoard, but not in CharacterCard. In fact, my mapping does not seem to be called at all in CharacterBoard when the props change.
Do I need to use something like getDerivedStateFromProps in CharacterBoard and set the state of that component and then use the state to map the values down to CharacterCard? If so, why?
In short (tl;dr), can you pass props on down through the component chain and map them out along the way and still have all changes reflected automatically?
Thanks for any guidance.
If it helps, the render method of my App is
render() {
const {state: {characters}} = this
return (
<div>
<header>
</header>
<main>
<CharacterBoard
onCardSelected={this.onCardSelected}
rowSize={logic.ROW_SIZE}
characters={characters}
cardSize={this.CARD_SIZE}/>
</main>
</div>
);
}
that of CharacterBoard is
render() {
const {props: {characters, rowSize, cardSize,onCardSelected}} = this
const rowUnit = 12 / rowSize
const cardLayout = characters
.map((character, i) => (
<Col xs={6} sm={rowUnit} key={character.name}>
<CharacterCard
onCardSelected = {onCardSelected}
key={i + Math.random()}
character={character}
cardSize={cardSize}
/>
</Col>
)
)
return (
<div>
<Container>
<Row>
{cardLayout}
</Row>
</Container>
</div>
)
}
and finally CharacterCard has this render method
render() {
const {props: {character, cardSize}} = this
const {thumbnail, revealed} = character
const imgURL = `${thumbnail.path}/${cardSize}.${thumbnail.extension}`
const topCardClass = classNames('characterCard__card-back', {'characterCard__card-back--hidden': revealed})
console.log(revealed)
return < a href="/#" onClick={this.onCardSelected}>
<div className='characterCard__card'>
<div className={topCardClass}>
<img src="/images/card_back.png" alt=""/>
</div>
< div className='characterCard__card-front'>< img alt=''
src={imgURL}/>
</div>
</div>
</a>
}
Doh! A simple forgetting to setState in App. Knowing that it should work made me go back through the code one more time and see that, indeed, it was a stupid error on my part.

ReactJS - Components - Best Practices

I'm relative new to react and I need to create a character counter component (CharacterCounter) for a input field.
This question is not specific to this component (character counter) - but is a generic question about components best practices in general.
I can implement in two ways (as far I know - if there are better ways I'm happy to hear about it):
1) Wrapping the component and have the input field as a child - The component will insert the span (for showing the counter) after the input field
<CharacterCounter maxLength={50}>
<input type="text">
</CharacterCounter>
and
const input = this.container.firstChild
input.addEventListener('keyup', function() { ... });
advantage: I can have multiple components for the same input - if I need extra functionality (components) for this input.
disadvantage: If the input for some reason stop being the first child of this component - stop working/ fragile
2) To create a generic component which will render the input and the counter on the render() function
like:
<CharacterCounter />
render() {
return (
<input type="text">
<span>{this.state.count}</span>
)
advantage: Not fragile - not relying on the first child
disadvantage: Not sure is possible to have other component for the same input - let's say I need another component for tracking every time the user type/ or focus/ or blur the field
What is the best practices?
Surely the second approach is better as it is not directly interfering with DOM elements.
If you wanted to have access to DOM elements, still it's better to use refs.
disadvantage: Not sure is possible to have other component for the
same input - let's say I need another component for tracking every
time the user type/ or focus/ or blur the field
You will get around that easily just with props.
You could use second approach with components with state and then use composition to extend that component or create more “special cases” of that component.
let {Component} = React;
class Input extends Component {
constructor(props) {
super(props);
this.state = {count: 0}
}
render() {
return <div>
<input
{...this.props}
onChange={() => {
let count = this.refs.inputCount.value.length;
this.setState({count})
}}
type="text"
maxLength="50"
ref="inputCount" />
<span> {this.state.count} / 50</span>
</div>
}
}
class FocusInput extends Component {
render() {
return <Input
onFocus={() => {
console.log('Focus')
}} />
}
}
class App extends Component {
render() {
return <div>
<Input />
<FocusInput />
</div>
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
<script src="https://unpkg.com/react#16.1.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.1.0/umd/react-dom.development.js"></script>
<div id="app"></div>

Categories