Unable to use usehistory in class component, example of withrouter - reactjs

I have below code:-
import React, { Component } from "react";
import { useHistory } from "react-router-dom";
class clusterServersDropdown extends Component {
constructor() {
super();
this.state = {
clusterslist: [],
servertype: [],
selectserver: "",
selectcluster: ""
};
}
componentDidMount() {
this.setState({
clusterslist: [
{ label: "cluster1", servertype: ["test1", "test2", "test3"] },
{ label: "cluster2", servertype: ["test1", "test2", "test3"] }
]
});
}
selectclusterChange(e) {
this.setState({ selectcluster: e.target.value });
this.setState({
servertype: this.state.clusterslist.find(
(x) => x.label === e.target.value
).servertype
});
}
routeChange = (e) => {
this.setState({ selectserver: e.target.value}, () => {
console.log(this.state.selectserver);
let path = "http://localhost:3000/inventory/cluster/" + this.state.selectcluster +"/servertype/" + this.state.selectserver;
console.log(path);
withRouter(path);
});
};
render() {
return (
<div>
<center>
<h1>
Implement cascading Dropdown list
<h2>
ReactJS tutorials
<hr />
<select
value={this.state.selectcluster}
onChange={this.selectclusterChange.bind(this)}
>
<option>-- Select --</option>
{this.state.clusterslist.map((x) => {
return <option>{x.label}</option>;
})}
</select>
<select
value={this.state.selectserver}
onChange={this.routeChange}
>
<option>--------selection disabled------</option>
{this.state.servertype.map((x) => {
return <option>{x}</option>;
})}
</select>
</h2>
</h1>
</center>
</div>
);
}
}
export default clusterServersDropdown;
Based on the output that I get i was trying to redirect to another link after creating the link here. When i do console.log my link gets printed as http://localhost:3000/inventory/cluster/cluster1/servertype/test1 to which I need to redirect. I have used usehistory in past but as its a hook, i am unable to use it here. Any ideas how can i use withrouter here?

withRouter is a Higher Order Component, import it and decorate the ClusterServersDropdown component.
import { withRouter } from "react-router-dom";
class ClusterServersDropdown extends Component {
...
}
export default withRouter(ClusterServersDropdown);
This injects route props (history, location, match) into your class component. Access the history object from props.
routeChange = (e) => {
this.setState({ selectserver: e.target.value}, () => {
console.log(this.state.selectserver);
let path = "http://localhost:3000/inventory/cluster/" + this.state.selectcluster +"/servertype/" + this.state.selectserver;
console.log(path);
this.props.history.push(path);
});
};

You can use hooks only in function components.
This is class component so you will need to use withRouter function when exporting clusterServersDropdown
export default withRouter(clusterServersDropdown);
and then you can use history object with
this.props.history

Related

How to mock a parent component which passes props to child component using react testing library

**How to check for the dynamic state changes in a parent component and write the test case using the react testing library if the props passed to the child component are based on the state changes which are happening dynamically in the parent component. Can someone help me with this?
App.js
import React, { Component } from 'react';
import './App.css';
import TextArea from './components/TextArea/TextArea';
class App extends Component {
constructor() {
super();
this.state = {
textAreaParams: {},
};
}
componentDidMount() {
this.setDefaultTextAreaMessage();
}
setDefaultTextAreaMessage = () => {
this.setState({
textAreaParams: { message: 'Hello' },
});
};
render() {
const { textAreaParams } = this.state;
return (
<div className="App">
{Object.keys(textAreaParams).length > 0 ? (
<TextArea params={textAreaParams} />
) : null}
</div>
);
}
}
export default App;
TextArea.js
import { Component } from 'react';
class TextArea extends Component {
constructor(props) {
super(props);
this.state = {
message: this.props.params.message,
};
}
render() {
return (
<div>
<textarea
rows="4"
cols="50"
value={this.state.message ? this.state.message : ''}
placeholder="test"
onChange={() => {}}
>
{this.state.message}
</textarea>
</div>
);
}
}
export default TextArea;
App.test.js
import App from './App';
import { cleanup, render } from '#testing-library/react';
describe('Rendering the App component and passing props to text area', () => {
afterEach(cleanup);
it('render the App component and check for the TextArea component', async () => {
const props = { textAreaParams: { message: 'save' } };
const { getByPlaceholderText } = render(<App {...props} />);
const textAreaParams = getByPlaceholderText('test');
expect(textAreaParams).toHaveTextContent('save');
});
});
We need to pass onChange handler prop from the App component to TextArea and then TextArea will component will call that handler when there is a change in the text area.
updateTextAreaMessage = (messageInTextArea) => {
this.setState({
textAreaParams: { message: messageInTextArea}
})
}
In the above code, messageInTextArea is a string value when we change the text in TextArea and when updateTextAreaMessage is called in the TextArea component with the same string value as a parameter, it will update the state in the App component.
Full Implementation:
App.js:
import React, { Component } from "react";
import './App.css';
import TextArea from './components/TextArea/TextArea';
class Main extends Component {
constructor() {
super();
this.state = {
textAreaParams: { message: "hello" } // we can provide default value here
};
}
updateTextAreaMessage = (messageInTextArea) => {
this.setState({
textAreaParams: { message: messageInTextArea }
});
};
render() {
const { textAreaParams } = this.state;
return (
<div className="App">
{Object.keys(textAreaParams).length > 0 ? (
<TextArea
params={textAreaParams}
onUpdate={this.updateTextAreaMessage}
/>
) : null}
<p aria-label="text area message">{textAreaParams.message}</p>
</div>
);
}
}
export default Main;
TextArea.js:
import { Component } from "react";
class TextArea extends Component {
render() {
return (
<div>
<textarea
rows="4"
cols="50"
value={this.props.params.message ? this.props.params.message : ""}
placeholder="test"
onChange={(event) => this.props.onUpdate(event.target.value)}
>
{this.props.params.message}
</textarea>
</div>
);
}
}
export default TextArea;
Now, we'll add the test for App.js. But the question is what to test here? The answer would we'll add the test for whether the state is updated or not when there is a change in the text of the TextArea component.
import { render } from "#testing-library/react";
import App from "./App";
import TextArea from './components/TextArea/TextArea';
describe("Rendering the App component and passing props to text area", () => {
it("should render the App component with default message in TextArea", () => {
const { getByPlaceholderText } = render(<Main />);
const textAreaParams = getByPlaceholderText("test");
expect(textAreaParams).toHaveTextContent(/hello/i);
});
it("should update the text area when we type something", () => {
const { getByPlaceholderText, getByLabelText } = render(<Main />);
userEvent.type(getByPlaceholderText("test"), "Anything");
expect(getByLabelText(/text area message/i)).toHaveTextContent(/anything/i);
});
});
describe("Rendering the Text Area component", () => {
it("should render the TextArea component and calls onChange handlers when we type something", () => {
const mockOnChangeHandler = jest.fn();
const mockParams = { message: "save" };
const { getByPlaceholderText } = render(
<TextArea params={mockParams} onUpdate={mockOnChangeHandler} />
);
const inputTextArea = getByPlaceholderText("test");
expect(inputTextArea).toHaveTextContent(/save/i);
userEvent.type(inputTextArea, "Anything");
expect(mockOnChangeHandler).toHaveBeenCalled();
});
});

Cannot update during an existing state transition without any setState in render()

I am learning react and I encountered this error and could not find the solution.
The error I recieve is:
Cannot update during an existing state transition (such as within
render). Render methods should be a pure function of props and
state. at clientsDisplay
(http://localhost:3000/static/js/main.chunk.js:672:39) at div at App
(http://localhost:3000/static/js/main.chunk.js:163:5)
The problem is in this code:
import React from 'react'
import Client from './Client/Client'
const clientsDisplay=(props)=>props.clientsArray.map(
(client,index) =>{
return <Client
clientName={client.name}
clientProjDeadline={client.deadline}
clickDel={props.clientDel(index)}
key={client.id}
changed={event=>props.changed(event,client.id)}
len={props.clArraylength}
/>}
)
export default clientsDisplay
The main component which contains the render function looks like this:
import appCss from './App.module.css';
import React,{Component} from 'react';
import ClientsDisplay from './components/ClientHandle/ClientsDisplay';
class App extends Component{
state = {
userName:'Julian',
showPara: false,
clientsArray: [
{
id:"001",
name: "Max",
deadline: "2021/05/17"
},
{
id:"002",
name: "James",
deadline: "2021/12/06"
},
{
id:"003",
name: "Johnny",
deadline: "2021/07/21"
}
]
}
deleteClient = (delClientIndex)=>{
let clientsArrayCopy=[...this.state.clientsArray]
clientsArrayCopy.splice(delClientIndex,1)
this.setState({
clientsArray:clientsArrayCopy
})
}
valueChanger = (event)=>{
this.setState({
userName: event.target.value
})
}
valueChangerClient = (event,id)=>{
let targetInd=this.state.clientsArray.findIndex(elem=>elem.id===id)
let changedClientArray=[...this.state.clientsArray]
changedClientArray[targetInd].name=event.target.value
this.setState({
clientsArray:changedClientArray
})
}
togglePara = ()=>{
this.setState({
showPara: !this.state.showPara
})
}
render(){
let clientArraylength=this.state.clientsArray.length
return(
<div className={appCss.App}>
<ClientsDisplay
clientsArray={this.state.clientsArray}
changed={this.valueChangerClient}
clientDel={this.deleteClient}
clArrayLength={clientArraylength}/>
</div>
)
}
Currently you're actually calling props.clientDel on every render:
clickDel={props.clientDel(index)}
should be
clickDel={() => props.clientDel(index)}

React, How to use a menu in a seperate file to call an api and return data to a different section of the main file

I have a react app with a large menu, and as such am trying to move it to a seperate file from the main app.js
at the mement when you click on a link in the menu it call a node api and which returns some data, however when I try to seperate I can not get it to populate the results section which is still in the main script
Working version app.js
import React,{ useState } from 'react';
import './App.css';
import axios from 'axios';
import { Navigation } from "react-minimal-side-navigation";
import "react-minimal-side-navigation/lib/ReactMinimalSideNavigation.css";
export default class MyList extends React.Component {
constructor(props) {
super(props);
this.state = {
result: [],
};
this.callmyapi = this.callmyapi.bind(this);
}
render() {
return (
<div>
<div class="menu">
<Navigation
onSelect={({itemId}) => {
axios.get(`/api/menu/`, {
params: {
Menu: itemId,
}
})
.then(res => {
const results = res.data;
this.setState({ results });
})
.catch((err) => {
console.log(err);
})
}}
items={[
{
title: 'Pizza',
itemId: '/menu/Pizza/',
},
{
title: 'Cheese',
itemId: '/menu/cheese',
}
]}
/>
</div>
<div class="body">
this.state.results && this.state.results.map(results => <li>* {results.Name}</li>);
</div>
</div>
);
}
}
New app.js
import React,{ useState } from 'react';
import './App.css';
//import axios from 'axios';
//import { Navigation } from "react-minimal-side-navigation";
//import "react-minimal-side-navigation/lib/ReactMinimalSideNavigation.css";
import MyMenu from './mymenu';
export default class MyList extends React.Component {
constructor(props) {
super(props);
this.state = {
result: [],
};
this.callmyapi = this.callmyapi.bind(this);
}
render() {
return (
<div>
<div class="menu">
<MyMenu />
</div>
<div class="body">
this.state.results && this.state.results.map(results => <li>* {results.Name}</li>);
</div>
</div>
);
}
}
New menu file
mymenu.js
import React, { Component } from 'react';
import axios from 'axios';
import './App.css';
//import MyList from './App.js';
//import { ProSidebar, Menu, MenuItem, SubMenu } from 'react-pro-sidebar';
//import 'react-pro-sidebar/dist/css/styles.css';
import { Navigation } from "react-minimal-side-navigation";
//import Icon from "awesome-react-icons";
import "react-minimal-side-navigation/lib/ReactMinimalSideNavigation.css";
//export default async function MyMenu(){
export default class MyMenu extends React.Component {
constructor(props) {
super(props);
};
render() {
return (
<div>
<Navigation
// you can use your own router's api to get pathname
activeItemId="/management/members"
onSelect={({itemId}) => {
// return axios
axios.get(`/api/menu/`, {
params: {
// Menu: itemId,
Menu: "meat",
SubMenu : "burgers"
}
})
.then(res => {
const results = res.data;
this.setState({ results });
})
.catch((err) => {
console.log(err);
})
}}
items={[
{
title: 'Pizza',
itemId: '/menu/Pizza/',
},
{
title: 'Cheese',
itemId: '/menu/cheese',
}
]}
/>
</div>
);
}
}
Any help would be greatly appreciated
That one is quite easy once you understand state. State is component specific it that case. this.state refers to you App-Component and your Menu-Component individually. So in order for them to share one state you have to pass it down the component tree like this.
export default class MyList extends React.Component {
constructor(props) {
super(props);
this.state = {
result: [],
};
}
render() {
return (
<div>
<div class="menu">
<MyMenu handleStateChange={(results: any[]) => this.setState(results)} />
</div>
<div class="body">
this.state.results && this.state.results.map(results => <li>* {results.Name}</li>);
</div>
</div>
);
}
}
See this line: <MyMenu handleStateChange={(results: any[]) => this.setState(results)} />
There you pass a function to mutate the state of App-Component down to a the child
There you can call:
onSelect={({itemId}) => {
// return axios
axios.get(`/api/menu/`, {
params: {
// Menu: itemId,
Menu: "meat",
SubMenu : "burgers"
}
})
.then(res => {
const results = res.data;
this.props.handleStateChange(results)
})
.catch((err) => {
console.log(err);
})
You mutate the parent state and the correct data is being rendered. Make sure to practice state and how it works and how usefull patterns look like to share state between components.
Thanks - I Have found solution (also deleted link question)
above render added function
handleCallback = (results) =>{
this.setState({data: results})
}
then where I display the menu
<MyMenu parentCallback = {this.handleCallback}/>
where i display the results
{this.state.results && this.state.results.map(results => <li>{results.Name}</li>}
No aditional changes to the menu scripts

React trying to make a list of dynamic inputs

I have built this site
https://supsurvey.herokuapp.com/surveycreate/
now I am trying to move the fronted to React so I can learn React in the process.
with vanila js it was much easier to create elements dynamically.
I just did createElement and after that when I clicked "submit" button
I loop throw all the elements of Options and take each target.value input.
so I loop only 1 time in the end when I click Submit and that's it I have now a list of all the inputs.
in react every change in each input field calls the "OnChange" method and bubbling the e.targe.value to the parent and in the parent I have to copy the current array of the options and rewrite it every change in every field.
is there other way? because it seems crazy to work like that.
Options.jsx
```import React, { Component } from "react";
class Option extends Component {
constructor(props) {
super(props);
this.state = { inputValue: "", index: props.index };
}
myChangeHandler = event => {
this.setState({ inputValue: event.target.value });
this.props.onChange(this.state.index, event.target.value);
};
render() {
return (
<input
className="survey-answer-group"
type="text"
placeholder="Add Option..."
onChange={this.myChangeHandler}
/>
);
}
}
export default Option;
______________________________________________________________________________
Options.jsx````
```import React, { Component } from "react";
import Option from "./option";
class Options extends Component {
render() {
console.log(this.props);
return <div>{this.createOptions()}</div>;
}
createOptions = () => {
let options = [];
for (let index = 0; index < this.props.numOfOptions; index++) {
options.push(
<Option key={index} onChange={this.props.onChange} index={index} />
);
}
return options;
};
}
export default Options;```
______________________________________________________________________________
App.jsx
```import React from "react";
import OptionList from "./components/Options";
import AddButton from "./components/add-button";
import "./App.css";
class App extends React.Component {
state = {
numOfOptions: 2,
options: [{ id: 0, value: "" }, { id: 1, value: "" }]
};
handleChange = (index, value) => {
const options = [...this.state.options];
console.log("from App", value);
options[index].value = value;
this.setState({
options: options
});
console.log(this.state);
};
addOption = () => {
const options = [...this.state.options];
options.push({ id: this.state.numOfOptions + 1, value: "" });
this.setState({
numOfOptions: this.state.numOfOptions + 1,
options: options
});
};
submitButton = () => {};
render() {
return (
<div className="poll-create-grid">
<div id="poll-create-options">
<OptionList
onChange={this.handleChange}
numOfOptions={this.state.numOfOptions}
/>
</div>
<button
className="surveyCreate-main-btn-group"
onClick={this.addOption}
>
Add
</button>
<button
className="surveyCreate-main-btn-group"
onClick={this.submitButton}
>
Submit
</button>
</div>
);
}
}
export default App;
```
So firstly,
The issue is with the way your OptionList component is defined.
Would be nice to pass in the options from the state into the component rather than the number of options
<OptionList
onChange={this.handleChange}
options={this.state.options}
/>
The you basically just render the options in the OptionsList component (I'm assuming it's same as the Options one here
class Options extends Component {
...
render() {
return
(<div>{Array.isArray(this.props.options) &&
this.props.options.map((option) => <Option
key={option.id}
index={option.id}
onChange={this.props.onChange}
value={option.value}
/>)}
</div>);
}
...
}
You would want to use the value in the Option component as well.
this.props.onChange(this.state.index, event.target.value); No need using the state here to be honest
this.props.onChange(this.props.index, event.target.value); is fine

React not reloading function in JSX

I am using react-redux.
I have the following JSX (only relevant snippets included):
getQuestionElement(question) {
if (question) {
return <MultiChoice questionContent={this.props.question.question} buttonClicked={this.choiceClicked} />
}
else {
return (
<div className="center-loader">
<Preloader size='big' />
</div>
)
}
}
render() {
return (
<div>
<Header />
{
this.getQuestionElement(this.props.question)
}
</div>
)
}
function mapStateToProps({ question }) {
return { question };
}
export default connect(mapStateToProps, questionAction)(App);
When the action fires, and the reducer updates the question prop
this.props.question
I expect
{this.getQuestionElement(this.props.question)}
to be reloaded and the new question rendered.
However this is not happening. Am I not able to put a function in this way to get it live reloaded?
My MultiChoice component:
import React, { Component } from 'react';
import ReactHtmlParser from 'react-html-parser';
import './questions.css';
class MultiChoice extends Component {
constructor(props) {
super(props);
this.state = {
question: this.props.questionContent.question,
answerArray : this.props.questionContent.answers,
information: null
}
this.buttonClick = this.buttonClick.bind(this);
}
createButtons(answerArray) {
var buttons = answerArray.map((element) =>
<span key={element._id} onClick={() => { this.buttonClick(element._id) }}
className={"span-button-wrapper-25 " + (element.active ? "active" : "")}>
<label>
<span>{element.answer}</span>
</label>
</span>
);
return buttons;
}
buttonClick(id) {
var informationElement;
this.props.buttonClicked(id);
var buttonArray = this.state.answerArray.map((element) => {
if (element._id === id ){
element.active = true;
informationElement = element.information;
return element;
}
else{
element.active = false;
return element;
}
});
this.setState({
answerArray: buttonArray,
information: informationElement
})
}
render() {
return (
<div className="question-container">
<div className="question-view">
<div className="icon-row">
<i className="fa fa-code" />
</div>
<div className="title-row">
{this.state.question}
</div>
<div className="button-row">
{this.createButtons(this.state.answerArray)}
</div>
<div className="information-row">
{ReactHtmlParser(this.state.information)}
</div>
</div>
</div>
);
}
}
export default MultiChoice;
QuestionAction.js
import axios from "axios";
import { FETCH_QUESTION } from "./types";
export const fetchQuestion = (questionId, answerId) => async dispatch => {
let question = null;
if (questionId){
question = await axios.get("/api/question/next?questionId=" + questionId + "&answerId=" + answerId);
}
else{
question = await axios.get("/api/question/next");
}
console.log("question", question);
dispatch({ type: FETCH_QUESTION, payload: question });
};
questionReducer.js
import {FETCH_QUESTION } from "../actions/types";
export default function(state = null, action) {
switch (action.type) {
case FETCH_QUESTION:
console.log("payload", action.payload.data);
return { question: action.payload.data, selected: false };
default:
return state;
}
}
index.js (Combined Reducer)
import { combineReducers } from 'redux';
import questionReducer from './questionReducer';
export default combineReducers({
question: questionReducer
});
and my entry point:
index.js
const store = createStore(reducers, {}, applyMiddleware(reduxThunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
registerServiceWorker();
requested console.log response:
render() {
console.log("Stackoverflow:", this.props.question)
.....
and after clicking the button (and the reducer updating, the console.log is updated, but the
this.getQuestionElement(this.props.question)
does not get re-rendered
MultiChoice Component shouldn't store his props in his state in the constructor, you have 2 options here :
Handle props changes in componentWillReceiveProps to update the state :
class MultiChoice extends Component {
constructor(props) {
super(props);
this.state = {
question: this.props.questionContent.question,
answerArray : this.props.questionContent.answers,
information: null
}
this.buttonClick = this.buttonClick.bind(this);
}
componentWillReceiveProps(nextProps) {
this.setState({
question: nextProps.questionContent.question,
answerArray : nextProps.questionContent.answers,
information: null
});
}
We have to keep using the constructor to set an initial state as from docs :
React doesn’t call componentWillReceiveProps() with initial props
during mounting.
2nd Option : Make it as a "dumb component" by having no state and only using his props to render something (some more deep changes in your component to do, especially to handle the "active" element, it will have to be handled by the parent component).

Resources