Setting mobX state from inside a React-Apollo component - javascript

When trying to set the onChange state of an input element, I am unable to change the state. React-Apollo components need a child function that returns some JSX. Inside the child function It appears that the thisobject is being inherited, but I cant get it to actually change.
If I remove the <Mutation/> component all together then everything works as expected. Is there something special about React-Apollo components or the way the this object interacts with arrow functions?
import React from 'react';
import gql from 'graphql-tag'
import { Mutation } from 'react-apollo';
import { extendObservable } from 'mobx';
import { observer } from 'mobx-react';
const mutation = gql`here is a mutation`
class Why extends React.Component{
constructor(props) {
super(props);
extendObservable(this, {
text: '',
})
}
onChange = e => {
this.text = e.target.value;
};
render () {
return (
<Mutation mutation={mutation}>
{ () => (
<div>
{console.log(this)}
<input name="text"
placeholder="Enter Text"
type="text"
value={this.text}
onChange={this.onChange}/>
</div>
)
}
</Mutation>
)
}
}
export default observer(Why);

I think the state is actually changing but the component render didn't react for you.
It happens because observer components only tracks data that directly accessed by the render function, in your case you have a function that don't.
a simple solution is to use the <Observer /> component from mobx-react:
render () {
return (
<Mutation mutation={mutation}>
{ () => (
<Observer>
{() => (
<div>
{console.log(this)}
<input name="text"
placeholder="Enter Text"
type="text"
value={this.text}
onChange={this.onChange}/>
</div>
)}
</Observer>
)
}
</Mutation>
)
}

Related

How to pass a value from a function to a class in React?

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.

Importing New React Component Compiles But Does Not Render

So I've been having problems with this code that i am working on
Dependencies: React, Redux, Eslinter, PropTypes, BreadCrumb
There's a view page that imports various components from other files
The Existing Structure Goes:
import Component from './Component.js';
...
let var = '';
...
if (this.state.value !=== null) {
var = <Component/>
}
...
render() {
return(
<div>
SomeContent
{var}
</ div>
)
}
When i try to import my created PureComponent the code Compiles.
Component Dependencies: { Button, Modal }React-Bootstrap, React, PropTypes
However the page does not render, and i cant figure out the reason why it would not render when it is introduced in the same manner as the existing structure Above
Update: I have tried making a bare minimum component returning just a simple Div & got the same result
RESOLVED:
There was a depreciated reference in my component to Modal from 'react-bootstrap' which still contained the node_module reference title but not the corresponding JS file which they have since moved to 'react-modal'
import Component from './Component.js';
let entity = '';
if (this.state.value !== null) {
entity = <Component />
}
render() {
return (
<div>
SomeContent
{entity}
</ div>
)
}
Firstly, You can not use var keyword for naming a variable and also, please check your condition fulfillment.
When dealing with state and conditional rendering, you should put conditions within the render method (or in a class field):
Working example:
import React from "react";
import OtherComponent from "../OtherComponent";
class Example extends React.Component {
constructor() {
super();
this.state = {
value: ""
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({ value: event.target.value });
}
render() {
return (
<div>
SomeContent
{this.state.value && <OtherComponent />}
<br />
<input
value={this.state.value}
placeholder="Type something..."
onChange={this.handleChange}
/>
</div>
);
}
}
export default Example;

React - change props at child1 with callback and pass new value to child2

In my app I have a child component, 'Menu', where a 'select' state is updated by a click event, like so:
Menus.jsx (child):
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import Brewing from './Brewing.jsx';
import { withRouter } from 'react-router-dom';
class Menus extends Component{
constructor (props) {
super(props);
this.state = {
select: '',
isLoading: false,
redirect: false
};
};
(...)
gotoCoffee = (index) => {
this.setState({isLoading:true, select:this.state.coffees[index]})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
console.log(this.state.coffees[index])
}
renderCoffee = () => {
if (this.state.redirect) {
return (<Redirect to={'/coffee/'+this.state.select} />)
}
}
render(){
const coffees = this.state.coffees;
return (
<div>
<h1 className="title is-1"><font color="#C86428">Menu</font></h1>
<hr/><br/>
{coffees.map((coffee, index) =>
<span key={coffee}>
<div>
{this.state.isLoading && <Brewing/>}
{this.renderCoffee()}
<div onClick={() => this.gotoCoffee(index)}
style={{textDecoration:'underline',cursor:'pointer'}}>
<strong><font color="#C86428">{coffee}</font></strong></div>
<div>
</div>
</div>
</span>)
}
</div>
);
}
}
export default withRouter(Menus);
the above works.
However, let's say I have another child component, 'Coffee', which should inherit this changed state.
I have learned that passing this event change, and state, from child to another child component, is an anti-pattern. Considering the ways of React, data can only flow from top-to-bottom i.e., from parent-to-child.
So have I tried to manage 'select' state from top to bottom, like so:
App.jsx (parent)
class App extends Component {
constructor() {
super();
this.state = {
select: '',
};
this.onSelectChange = this.onSelectChange.bind(this);
};
then I would use a callback here at 'App.jsx', like so:
onSelectChange(newSelect){
this.setState({ select: newSelect });
}
and pass it to 'Menus' component, like so:
<Route exact path='/menus' render={() => (
<Menus
onSelectChange={this.onSelectChange}
/>
)} />
finally, at child 'Menus', I would user event change to change props, which could be passed to other childs etc:
gotoCoffee = (index) => {
this.setState({isLoading:true})
this.props.onSelectChange(index)
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
console.log(this.props.select)
}
but I'm getting console.log(this.props.select) 'undefined'.
what am I missing?
You are only passing onSelectChange method as a prop to Menu component right now, to access this.props.select, you need to pass select as prop to Menu.
<Route exact path='/menus' render={() => (
<Menus
onSelectChange={this.onSelectChange}
select={this.state.select}
/>
)} />
Whenever this.onSelectChange method gets called and state changes in your App.jsx, your Menu component will be rendered. You can use the updated this.props.select in your render method or in any non static method of your Menu component.
class Menu extends Component {
render() {
console.log(this.props.select);
return (
...
);
}
}

How to preserve referencies when Component is exported via a function?

Documentation describes how to add a ref to a class component when using ReactJS version 16.3+.
Here is a simplified and working example using two files:
MyForm.js file:
import React, { Component } from 'react';
import MyInput from "./MyInput";
class MyForm extends Component {
constructor(props) {
super(props);
this.myInput = React.createRef();
this.onClick = this.onClick.bind(this);
}
onClick(){
console.log(this.myInput.current.isValid());
}
render() {
return (
<div>
<MyInput ref={this.myInput} />
<button onClick={this.onClick}>Verify form</button>
</div>
);
}
}
export default MyForm;
MyInput.js file
import React, { Component } from 'react';
class MyInput extends Component {
isValid(){
return true;
}
render() {
return (
<div>
Name :
<input type="text" />
</div>
);
}
}
export default MyInput;
It works fine, console displays true when I click on MyForm button. But as soon as I add a function just before exporting my Component, errors are thrown. As example, I add a translation via react-i18n
MyInput.js file with export using a function
class MyInput extends Component {
isValid(){
return true;
}
render() {
const {t} = this.props;
return (
<div>
{t("Name")}
<input type="text" />
</div>
);
}
}
export default translate()(MyInput); // <=== This line is changing
Now, when I click on button, an error is thrown:
TypeError: this.myInput.current.isValid is not a function
The error disappear when I remove translate() in the last line.
I understood that the ref has been destroyed by the new component returned by translate function. It's an HOC. I read the Forwarding ref chapter, but I don't understand how to forward ref to the component returned by translate() function.
I have this problem as soon as I use translate from reacti18next and with the result of connect function from redux
I found a solution using onRef props and ComponentDidMount, but some contributors thinks this is an antipattern and I would like to avoid this.
Is there a way to create a wrapper that catch the HOC result of translate() or connect() and add ref to this HOC result ?

How to access props.history correctly in a wrapper that is not a Route component in React? [duplicate]

How can I move to a new page after some validation is done with React Router V4? I have something like this:
export class WelcomeForm extends Component {
handleSubmit = (e) => {
e.preventDefault()
if(this.validateForm())
// send to '/life'
}
render() {
return (
<form className="WelcomeForm" onSubmit={this.handleSubmit}>
<input className="minutes" type="number" value={this.state.minutes} onChange={ (e) => this.handleChanges(e, "minutes")}/>
</form>
)
}
}
I would like to send the user to another route. I had a look at Redirect but it seems like it would delete the current page from the history which I don't want.
You are using react-router v4, so you need to use withRouter with your component to access the history object’s properties, then use history.push to change the route dynamically.
withRouter:
You can get access to the history object’s properties and the closest
's match via the withRouter higher-order component. withRouter
will re-render its component every time the route changes with the
same props as render props: { match, location, history }.
Like this:
import {withRouter} from 'react-router-dom';
class WelcomeForm extends Component {
handleSubmit = (e) => {
e.preventDefault()
if(this.validateForm())
this.props.history.push("/life");
}
render() {
return (
<form className="WelcomeForm" onSubmit={this.handleSubmit}>
<input className="minutes" type="number" value={this.state.minutes} onChange={ (e) => this.handleChanges(e, "minutes")}/>
</form>
)
}
}
export default withRouter(WelcomeForm);
You can use withRouter higher-order component which will inject history object as property. Then you can use history.push to make redirection:
import { withRouter } from 'react-router-dom';
...
class WelcomeForm extends Component {
handleSubmit = (e) => {
e.preventDefault()
if(this.validateForm())
this.props.history.push('/life');
}
render() {
return (
<form className="WelcomeForm" onSubmit={this.handleSubmit}>
<input className="minutes" type="number" value={this.state.minutes} onChange={ (e) => this.handleChanges(e, "minutes")}/>
</form>
)
}
}
export default withRouter(WelcomeForm);
To make redirection you can also use <Redirect to="/someURL" /> in some cases but this component have to be rendered so you have to use it somewhere in JSX.
Depending on how you want your redirect to behave there are several options: React router docs
Redirect component
Rendering a will navigate to a new location. The new location will override the current location in the history stack, like server-side redirects (HTTP 3xx) do.
to: string - The URL to redirect to.
to: object - A location to redirect to.
push: bool - When true, redirecting will push a new entry onto the history instead of replacing the current one.
Example: <Redirect push to="/somewhere"/>
import { Route, Redirect } from 'react-router'
export class WelcomeForm extends Component {
handleSubmit = (e) => {
e.preventDefault()
if(this.validateForm())
<Redirect push to="/life"/>
}
render() {
return (
<form className="WelcomeForm" onSubmit={this.handleSubmit}>
<input className="minutes" type="number" value={this.state.minutes} onChange={ (e) => this.handleChanges(e, "minutes")}/>
</form>
)
}
}
Using withRouter HoC
This higher order component will inject the same props as Route. However, it carries along the limitation that you can have only 1 HoC per file.
import { withRouter } from 'react-router-dom'
export class WelcomeForm extends Component {
handleSubmit = (e) => {
e.preventDefault()
if(this.validateForm())
this.props.history.push("/life");
}
render() {
return (
<form className="WelcomeForm" onSubmit={this.handleSubmit}>
<input className="minutes" type="number" value={this.state.minutes} onChange={ (e) => this.handleChanges(e, "minutes")}/>
</form>
)
}
}
If you are using TypeScript extends your component using React.Component<RouteComponentProps> to get this.props.history.push properly
class YourComponent extends React.Component<RouteComponentProps> {
constructor(props: Readonly<RouteComponentProps>) {
super(props)
}
public render(){
// you can access history programmatically anywhere on this class
// this.props.history.push("/")
return (<div/>)
}
}
return default withRouter(YourComponent)

Categories

Resources