I am a bit of a react newbie so please be gentle on me. I am trying to build an education app, and code is to render multiple choice answer boxes:
export default function AnswerBox(props: any) {
return (
<div className="answer-container">
<ul>
{props.answers.map((value: any) => {
return (
<li className="answer-box" key={value.letter} id={value.letter}>
<input className="answer-textbox" type="checkbox" onChange={() => console.log('selected: ', value.letter)}></input>
<span className="answer-letter"><b>{value.letter})</b></span>
{value.answer}
</li>)
})
}
</ul>
</div>
)
}
As you can see, the function takes a object of arrays and iterates through arrays to display the Question Letter (e.x. 'A') and Question Answer in an unordered list.
So all of this is good, but I'd like the list element to be highlighted or have the answerbox div changed when it is actually selected. And I havent found a good way to do that other than to change the component into a stateful component and use a state var to track which box is ticked.
But when I changed it to a stateful component last night, I really struggled to pass in the list of objects to render.
How can I have a stateful class that also accepts props like a regular functional component?
To begin, you pass props to all types of components in a similar fashion, regardless if it's stateful or not.
<Component prop={someValue}/>
The only difference is how you would access them.
For class-based components you would access them through the props property of the class this.props. i.e.
class Component extends React.Component {
constructor(props) {
// you need to call super(props) otherwise
// this.props will be underfined
super(props);
}
...
someFunction = (...) => {
const value = this.props.prop;
}
}
If you're using TypeScript, you need to describe to it the structure of your props and state like this
interface iComponentProps {
prop: string;
};
interface iComponentState { ... };
export default class Component extends React.Component<iComponentProps, iComponentState> {
...
}
if your component takes in props and/or state and you're unsure of their structure, pass in any for the one you're unsure of.
On the other hand, if I understood your question correctly, you could do something like this:
I also made a demo of the simple app I made to address your other question.
In summary, you can have your AnswerBox component maintain an array of indexes that pertain to each of its choices and have it updated every time a choice is selected (or clicked) by using setState
You can also check out the useState hook to make your functional component stateful.
App.js
import React from "react";
import "./styles.css";
import Question from "./Question";
export default function App() {
const questionData = [
{
question: "Some Question that needs to be answered",
choices: ["Letter A", "Letter B", "Letter C"]
},
{
question: "Another Question that needs to be answered",
choices: ["Letter A", "Letter B", "Letter C"]
}
];
return (
<div className="App">
{questionData.map(question => (
<Question questionText={question.question} choices={question.choices} />
))}
</div>
);
}
Question.js
import React from "react";
import AnswerBox from "./AnswerBox";
const Question = ({ questionText, choices }) => {
return (
<div className={"question-container"}>
<p className={"question-text"}>{questionText}</p>
<AnswerBox choices={choices} />
</div>
);
};
export default Question;
QuestionChoice.js
import React from "react";
import clsx from "clsx";
const QuestionChoice = ({ letter, content, isSelected, handleClick }) => {
return (
<li
className={clsx("question-choice-container", {
"selected-choice": isSelected
})}
>
<input type={"checkbox"} value={content} onClick={handleClick} />
<label for={content} className={"question-choice-label"}>
<strong>{letter.toUpperCase()}. </strong>
<span>{content}</span>
</label>
</li>
);
};
export default QuestionChoice;
AnswerBox.js
import React, { PureComponent } from "react";
import QuestionChoice from "./QuestionChoice";
export default class AnswerBox extends PureComponent {
constructor(props) {
super(props);
this.choiceLetters = Array.from("abcdefghijklmnopqrstuvwxyz");
this.state = { activeChoices: [] };
}
_updateActiveChoices = index => {
let updatedList = [].concat(this.state.activeChoices);
if (this.state.activeChoices.indexOf(index) !== -1) {
updatedList.splice(updatedList.indexOf(index), 1);
} else {
updatedList.push(index);
}
return updatedList;
};
_handleChoiceSelect = choiceIndex => () => {
// an update to your component's state will
// make it re-run its render method
this.setState({ activeChoices: this._updateActiveChoices(choiceIndex) });
};
render() {
return (
<ul class={"answer-box"}>
{this.props.choices.map((choice, index) => (
<QuestionChoice
letter={this.choiceLetters[index]}
content={choice}
isSelected={this.state.activeChoices.indexOf(index) != -1}
handleClick={this._handleChoiceSelect(index)}
/>
))}
</ul>
);
}
}
For this type of interaction you can use the input checkbox's checked attribute to show that its checked. The check state should be derived from somewhere in your state. In the onClick function, you can look at the event for the name and checked state to update your state. Note you could add any attribute you want if name is too generic for you.
The interaction could look like this:
https://codesandbox.io/s/optimistic-archimedes-ozr72?file=/src/App.js
Related
Background
I wrote an exact, short yet complete example of a Parent component with a nested Child component which simply attempts:
Alter a string in the Parent's state
See the Child component updated when the Parent's state value is altered (this.state.name)
Here's What It Looks Like
When the app loads a default value is passed from Parent state to child props.
Change The Name
All I want to do is allow the change of the name after the user adds a new name in the Parent's <input> and clicks the Parent's <button>
However, as you can see, when the user clicks the button only the Parent is rendered again.
Questions
Is it possible to get the Child to render the new value?
What am i doing wrong in this example -- why isn't it updating or
rendering the new value?
All Source Code
Here is all of the source code and you can view it and try it in my StackBlitz project.
I've kept it as simple as possible.
Parent component (DataLoader)
import * as React from 'react';
import { useState } from 'react';
import { Grid } from './Grid.tsx';
interface LoaderProps {
name: string;
}
export class DataLoader extends React.Component<LoaderProps, {}> {
state: any = {};
constructor(props: LoaderProps) {
super(props);
this.state.name = this.props.name;
this.changeName = this.changeName.bind(this);
}
render() {
const { name } = this.state;
let parentOutput = <span>{name}</span>;
return (
<div>
<button onClick={this.changeName}>Change Name</button>
<input id="mapvalue" type="text" placeholder="name" />
<hr id="parent" />
<div>### Parent ###</div>
<strong>Name</strong>: {parentOutput}
<hr id="child" />
<Grid childName={name} />
</div>
);
}
changeName() {
let newValue = document.querySelector('#mapvalue').value.toString();
console.log(newValue);
this.setState({
name: newValue,
});
}
}
Child component (Grid)
import * as React from 'react';
interface PropsParams {
childName: string;
}
export class Grid extends React.Component<PropsParams, {}> {
state: any = {};
constructor(props: PropsParams) {
super(props);
let counter = 0;
this.state = { childName: this.props.childName };
console.log(`CHILD -> this.state.name : ${this.state.childName}`);
}
render() {
const { childName } = this.state;
let mainChildOutput = <span>{childName}</span>;
return (
<div>
<div>### Child ####</div>
<strong>Name</strong>: {mainChildOutput}
</div>
);
}
}
App.tsx is set up like the following -- this is where default value comes in on props
import * as React from 'react';
import { DataLoader } from './DataLoader.tsx';
import './style.css';
export default function App() {
return (
<div>
<DataLoader name={'default value'} />
</div>
);
}
You're seeing two different values because you're tracking two different states. One in the parent component and one in the child component.
Don't duplicate data.
If the child component should always display the prop that's passed to it then don't track state in the child component, just display the prop that's passed to it. For example:
export class Grid extends React.Component<PropsParams, {}> {
render() {
const { childName } = this.props; // <--- read the value from props, not local state
let mainChildOutput = <span>{childName}</span>;
return (
<div>
<div>### Child ####</div>
<strong>Name</strong>: {mainChildOutput}
</div>
);
}
}
In the Child component, you set the prop childName value to state in the contructor ONLY. The constructor is executed ONLY WHEN THE COMPONENT IS MOUNTED. So, it doesn't know if the childName prop is changed later.
There are 2 solutions for this.
(1) Directly use this.props.childName without setting it to a state.
(2) Add a useEffect that updates the state value on prop change.
React.useEffect(() => {
this.state = {
childName: this.props.childName;
};
}, [this.props.childName]);
However, I recommend 1st solution since it's not a good practice to duplicate data.
Goal
I am aiming to get the transcript value, from the function Dictaphone and pass it into to the SearchBar class, and finally set the state term to transcript.
Current code
import React from 'react';
import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition';
const Dictaphone = () => {
const { transcript } = useSpeechRecognition()
if (!SpeechRecognition.browserSupportsSpeechRecognition()) {
return null
}
return (
<div>
<button onClick={SpeechRecognition.startListening}>Start</button>
<p>{transcript}</p>
</div>
)
}
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
term: ''
}
this.handleTermChange = this.handleTermChange.bind(this);
}
handleTermChange(event) {
this.setState({ term: event.target.value });
}
render() {
return (
<div className="SearchBar">
<input onChange={this.handleTermChange} placeholder="Enter some text..." />
<Dictaphone />
</div>
)
}
}
export { SearchBar };
Problem
I can render the component <Dictaphone /> within my SearchBar. The only use of that is it renders a button and the transcript. But that's not use for me.
What I need to do is, get the Transcript value and set it to this.state.term so my input field within my SearchBar changes.
What I have tried
I tried creating an object within my SearchBar component and called it handleSpeech..
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
term: ''
}
this.handleTermChange = this.handleTermChange.bind(this);
}
handleTermChange(event) {
this.setState({ term: event.target.value });
}
handleSpeech() {
const { transcript } = useSpeechRecognition()
if (!SpeechRecognition.browserSupportsSpeechRecognition()) {
return null
}
SpeechRecognition.startListening();
this.setState({ term: transcript});
}
render() {
return (
<div className="SearchBar">
<input onChange={this.handleTermChange} placeholder="Enter some text..." />
<button onClick={this.handleSpeech}>Start</button>
</div>
)
}
}
Error
But I get this error:
React Hook "useSpeechRecognition" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
React Hooks must be called in a React function component or a custom React Hook function
Well, the error is pretty clear. You're trying to use a hook in a class component, and you can't do that.
Option 1 - Change SearchBar to a Function Component
If this is feasible, it would be my suggested solution as the library you're using appears to be built with that in mind.
Option 2
Communicate between Class Component <=> Function Component.
I'm basing this off your "current code".
import React, { useEffect } from 'react';
import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition';
const Dictaphone = ({ onTranscriptChange }) => {
const { transcript } = useSpeechRecognition();
// When `transcript` changes, invoke a function that will act as a callback to the parent (SearchBar)
// Note of caution: this code may not work perfectly as-is. Invoking `onTranscriptChange` would cause the parent's state to change and therefore Dictaphone would re-render, potentially causing infinite re-renders. You'll need to understand the hook's behavior to mitigate appropriately.
useEffect(() => {
onTranscriptChange(transcript);
}, [transcript]);
if (!SpeechRecognition.browserSupportsSpeechRecognition()) {
return null
}
return (
<div>
<button onClick={SpeechRecognition.startListening}>Start</button>
<p>{transcript}</p>
</div>
)
}
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
transcript: ''
}
this.onTranscriptChange = this.onTranscriptChange.bind(this);
}
onTranscriptChange(transcript){
this.setState({ transcript });
}
render() {
return (
<div className="SearchBar">
<input onChange={this.handleTermChange} placeholder="Enter some text..." />
<Dictaphone onTranscriptChange={onTranscriptChange} />
</div>
)
}
}
useSpeechRecognition is a React hook, which is a special type of function that only works in specific situations. You can't use hooks inside a class-based component; they only work in function-based components, or in custom hooks. See the rules of hooks for all the limitations.
Since this hook is provided by a 3rd party library, you have a couple of options. One is to rewrite your search bar component to be a function. This may take some time if you're unfamiliar with hooks.
You can also see if the react-speech-recognition library provides any utilities that are intended to work with class-based components.
So my question is a simple one. In React js I want to pass some states and handlers from a parent to its 3rd grandchild using Context. I have implemented this within the jsx but I want to use the states within the javascript o that I have some logic before I completely output my states.
I have divided my question into 2 parts. 1.) What I have done so far. 2.) What I want to do essentially.
1.)
// this file just stores the Context
MyContext.js
import React, { Component } from 'react';
export const MyContext = React.createContext();
MyProvider.js // this class is used by the parent and the child to have access to the provider
import React, { Component } from 'react';
import {MyContext} from '../MyContext'
class MyProvider extends Component {
state = {
name: 'Wes',
age: 100,
cool: true
}
render() {
return (
<MyContext.Provider value={{
state: this.state,
growAYearOlder: () => this.setState({
age: this.state.age + 1
})
}}>
{this.props.children}
</MyContext.Provider>
)
}
}
export default MyProvider;
// Ok so now I am basically skipping the parent and showing you the consumer grand-child
Person.js
import React, { Component } from 'react';
// first we will make a new context
import { MyContext } from '../MyContext';
class Person extends Component {
render() {
return (
<div className="person">
<MyContext.Consumer>
{(context) => (
<React.Fragment>
<p>Age: {context.state.age}</p>
<p>Name: {context.state.name}</p>
<button onClick={context.growAYearOlder}>π°π₯π</button>
</React.Fragment>
)}
</MyContext.Consumer>
</div>
)
}
}
export default Person;
2.)
// Ok so as you can see here I have had to immediately use the context.growAYearOlder. What I want to do instead is have control of it using javascript and modify it as desired; So something like this:
Child.js
const parentContext = MyContext.getContext();
if(somethingHappens){
parentContext().growAYearOlder();
}
return(
// The now rendered component
);
I tried something like this but it doesnt work:
MyContext.Consumer.context.growAYearOlder();
There are many similar questions with proper answers, docs, examples and so on - but this question kept popping up for me.
So, in case you want to get the context value and use it within your component's render() just import it (export context itself not only provider) and use _currentValue e.g.
const contextData = MyContext._currentValue;
Note that you still have to wrap your components with your given context provider.
Also note that for function components, you need to use useContext e.g.
const contextData = useContext(MyContext);
And for class components you can assign the context to a static var and then use it e.g.
class Main extends React.Component(){
static contextType = MyContext;
componentDidMount(){
const contextData = this.context;
}
render() {
return (
<p>Hey</p>
);
}
Note that the static var has to be called contextType otherwise this.context won't hold the MyContext data.
I've based my answer solely from the docs itself(https://reactjs.org/docs/context.html#updating-context-from-a-nested-component)
import React, { Component } from 'react';
import { MyContext } from '../MyContext'
class MyProvider extends Component {
constructor(props) {
super(props)
// I've moved the state declaration inside the constructor
this.state = {
name: 'Wes',
age: 100,
cool: true
}
// moved the function here and added prevState
this.growAYearOlder = () => {
this.setState(prevState => ({
age: prevState.age + 1,
}))
};
}
render() {
return (
<MyContext.Provider value={{
state: this.state,
growAYearOlder: this.growAYearOlder,
}}>
{this.props.children}
</MyContext.Provider>
)
}
}
export default MyProvider;
I'm building simple todo app in react and I have made input field as part of inputForm element which is child element.
I can pass functions as props from parent to child without problem, but I can't update parent state to store value on input field. When I type in input field, passed function is executing normally but currentTodo state is not updating.
I have found that this problem can be avoided by using single data flow pattern (like Flux or Reflux) but as this is my first project I want to understand how to work with basics.
Code for parent element:
import React, { Component } from 'react';
import './App.css';
import InputForm from '../components/InputForm'
import {Task} from '../components/Task'
class App extends Component {
constructor(){
super();
this.state = {
tasks: ["Todo", "Toda"],
currentToDo: "",
};
}
//makes copy of task array, pushes current to do to copy and setsState
//with new values
addTodo = () => {
console.log("addTodo")
let copy = this.state.tasks.slice();
console.log(this.state.currentToDo)
copy.push(this.state.currentToDo);
this.setState({tasks: copy});
}
//gets input value from input field and updates current todo
onInputChange = e => {
console.log(e.target.value);
this.setState({ currentTodo: e.target.value })
}
render() {
let drawTask = this.state.tasks.map(e => {
return <Task todo={e}/>
})
return (
<div className="container">
<InputForm onInputChange={() => this.onInputChange} add={this.addTodo}/>
{drawTask}
</div>
);
}
}
export default App;
Code for child element:
import React, { Component } from 'react';
import './component.css';
import {AddButton} from './Buttons.js'
class InputForm extends Component{
constructor(){
super();
this.state = {
}
}
render(){
return(
<div className='taskHeader'>
{/*Value of current todo is send as props from parent element*/}
<input value = {this.props.currentToDo} onChange={this.props.onInputChange()} type="text"/>
<AddButton add = {this.props.add}/>
</div>
)
}
}
export default InputForm;
You are calling the function during the render rather than passing a reference.
Parent owns the function and needs to pass it to the child:
<InputForm onInputChange={this.onInputChange} add={this.addTodo}/>
Now that the child has a prop called onInputChange, you pass it to the onChange callback as a reference.
<input value={this.props.currentToDo} onChange={this.props.onInputChange} type="text"/>
I am making a Higher-Order Component in my React.js (+ Redux) app, to abstract the functionality to filter a list of elements with the string received from an input element.
My filtering HOC is,
filter.js
import React, { Component } from 'react'
export default function Filter(FilteredComponent) {
return class FilterComponent extends Component {
constructor(props) {
super(props)
}
generateList() {
if (this.props.searchTerm !== undefined) {
let re = new RegExp(state.searchTerm,'gi')
return this.props.currencyList.filter((c) => c.match(re))
}
else {
return this.props.currencyList
}
}
render() {
return (
<FilteredComponent
filteredList={this.generateList()}
{...this.props}
/>
)
}
}
}
Right now, I am unable to access the filteredList as props.filteredList in the SearchResults component.
The component to display the list is
SearchResults.js
import React from 'react'
const SearchResults = (props) => {
const listData = props.filteredList.map (item => <div>{item}</div>)
return (
<div>
Here are the search results.
<br />
<input
type="text"
value={props.searchTerm}
onChange={props.setSearchTerm}
/>
{listData}
</div> ) }
export default SearchResults
How do I go on about this?
EDIT:
Adding the container component for greater clarity:
SearchContainer.js
import {connect} from 'react-redux'
import SearchResults from '../components/SearchResults'
import * as a from '../actions'
import Filter from '../enhancers/filter'
const getSearchTerm = (state) => (state.searchTerm === undefined) ? '' : state.searchTerm
const mapStateToProps = (state) => {
return {
searchTerm: getSearchTerm(state),
currencyList: state.currencyList
}
}
const mapDispatchToProps = (dispatch) => {
return {
setSearchTerm: (e) => {
dispatch(a.setSearchTerm(e.target.value))
}
}
}
const SearchResultsContainer = connect(
mapStateToProps,
mapDispatchToProps
)(SearchResults)
export default Filter(SearchResultsContainer)
Letβs first think of components as a function that takes a props and returns a Virtual DOM.
Thus the SearchResult component takes these props:
filteredList
searchTerm
setSearchTerm
The higher-order-component created created by connect() provides these props:
searchTerm
currencyList
The Filter() higher-order component:
takes currencyList
provides filteredList
Therefore, you have to wire it like this so that each part receives the props it needs:
connect(...) β Filter β SearchResult
It should look like this:
export default connect(...)(Filter(SearchResult))
Or if you use recompose:
const enhance = compose(connect(...), Filter)
export default enhance(SearchResult)
compose() wraps the components from right to left. Therefore, the leftmost higher-order component becomes the outermost one. This means the props will flow from left to right.
Please note that state.searchTerm in FilterComponent#generateList should be this.props.searchTerm.
What is 'state.searchTerm' in your wrapper function? I have a feeling you mean this.props.searchTerm. Also, you don't need an empty constructor in es6 classes. Also, this is work better done by a selector in your mapstatetoprops on the container.
Edit:
Also, you need to wrap the actual 'dumb' component, not the result of your connect call. That way your redux store is connected to your Filter component and will be rerendered when you're store changes.
generateList() is not reactive. It does not get triggered when the search term is changed.
SearchResults should be stateful and the container component. The list component should respond to change in the search term by receiving the search term as props. generateList should be the functionality of componentWillReceiveProps of the list component.