I tried to write unit test with enzyme to component that wrapped to redux-form w, I met a problem when try to test some component that renders depends on formValues HOC from redux-form HOC, how to test and simulate store or props from fromValues to component. Sure I have some parent element that wrapped to HOC redux-form
export const MiConfiguration = ({miConfiguration, change}) =>
{
miConfiguration.isMiEnabled = miConfiguration.miConfigurationType !== MiConfigurationTypes.AccessPointOnly
return <FormSection name='miConfiguration'>
<Field
name='miConfigurationType'
component={renderSelectField}
label={<FormattedMessage id='passage.label.scenario' />}
style={{width: 400, ...styles.selectField}}
hintText={<FormattedMessage id='passage.selectConfiguration.hint'/>}
autoWidth
onChange={(e, newValue) => Object.keys(defaultValues[newValue]).forEach(key => change(key, defaultValues[newValue][key]))}
>
{miConfiguration && !!miConfiguration.miConfigurationType &&
<InfoMessage id={`miConfiguration.description.${miConfiguration.miConfigurationType}`} />}
</FormSection>}
Unit Test
describe('getMiConfiguration', () => {
let component, onChange
beforeEach(() => {
component = shallow(<MiConfiguration miConfiguration={{}} change={onChange = sinon.spy()}/>)
})
it('should render <InfoMessage /> with id', () =>{
component.setProps({miConfiguration: {miConfigurationType: 'type'})
component.find(InfoMessage).props().id.should.be.equal('some id')
component.find(InfoMessage).should.exist})
})
;[{id: MiConfigurationTypes.AccessPointOnly},
{id: MiConfigurationTypes.WanderingDetection},
{id: MiConfigurationTypes.MuteWanderingDetection},
{id: MiConfigurationTypes.LockedWanderingControl},
{id: MiConfigurationTypes.OpenWanderingControl},
].forEach(({id}) => {
it(`should render correct <InfoMessage/> with ${id} for each MI configuration scenarios id`, () => {
component.setProps({miConfiguration: {miConfigurationType: id}})
component.find(InfoMessage).findWhere(x => x.props().id === `miConfiguration.description.${id}`).should.exist
})
})
Related
Codesandbox here
I am using context to the state of various components, in this case, which checkbox is checked, and am using that component to mutate that component state. However, by clicking only one checkbox, all four components (checkboxes) rerender. How can I prevent this rerendering when hooking into and mutating context? Thanks.
index.tsx:
import * as React from "react";
import ReactDOM from "react-dom";
import { ContextProvider, useMyContext } from "./context";
import "./styles.css";
const Checkbox: React.FC<{ id: number }> = ({ id }) => {
React.useEffect(() => console.log(`Checkbox ${id} Render`));
const { setValue, value } = useMyContext();
return (
<input
onClick={() => setValue(id)}
checked={id === value}
type="checkbox"
/>
);
};
const Container: React.FC = () => {
React.useEffect(() => console.log("Container Render"));
return (
<div>
{[0, 1, 2, 3].map(id => (
<Checkbox id={id} />
))}
</div>
);
};
const App: React.FC = () => {
return (
<ContextProvider>
<Container />
</ContextProvider>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
context.tsx:
import * as React from "react";
interface ContextState {
setValue: (id: number) => void;
value: number;
}
const initialContextState: ContextState = {
setValue: () => {},
value: 0
};
const Context = React.createContext<ContextState>(initialContextState);
export const ContextProvider: React.FC = (props: object) => {
const [value, setValue] = React.useState<number>(0);
return (
<Context.Provider
value={{
setValue,
value
}}
{...props}
/>
);
};
export const useMyContext = (): ContextState => React.useContext(Context);
You can't prevent the rerender of the context-consuming component (Checkbox in your example) - the forced rerender when the context value is updated is just how the Context API works, you can't apply any 'selectivity' to it.
What you can do is take the expensive part of the actual content of that consuming component, extract it into a child component wrapped with React.memo (or inline it as a chunk of JSX wrapped in a useMemo hook - docs), get the values you need from the context and pass those as props to that child component / dependencies to the useMemo section.
The built in memoisation will then 'just work' and will not rerender the child component (the expensive part) for the checkboxes whose actual value doesn't update, as the relevant props will not change.
Using memoisation like this can solve any actual performance issues you need to address, but just a reminder that 90% of the time, this stuff just doesn't matter. Test and determine performance is actually an issue before you refactor your components in this way. It's usually simply not worth the extra indirection to solve a non-problem!
Here you can find a similar example,
using this library react-hooks-in-callback you can filter out all unnecessary re-renders
check the result Here
I'm trying to reproduce the React-Admin DEMO with functionality on how the review list functions.
In there, is a List of items and once a row is clicked, the Show view appears on the right. Notice below that my code snippet is very similar to the demo example, but the Drawer doesn't open and instead the Show view opens on a new page.
Please note a key difference:
My List and Datagrid fetch data via different resources, and am able to combine the two using a ReferenceManyField.
The List render employees, and the Show should render a list of the employee's timesheet.
// I think my problem is that, this always returns "false"
const isMatch = !!(
match &&
match.params &&
match.params.id !== 'create'
);
Here's my code, and thanks for any help:
class NestedTimesheetList extends React.Component {
handleClose = () => {
this.props.push('/timesheets');
};
render() {
const { push, classes, ...props } = this.props;
return (
<div className={classes.root}>
<Route path="/timesheets/show/:id">
{({ match }) => {
const isMatch = !!(match && match.params && match.params.id !== 'create');
let details = (id, basePath, record) => (
// triggered by rowClick
linkToRecord('/TimesheetSummary', record.id, 'show') +
`?employeeId=${props.id}&employeeCode=${props.record.employeeCode}`
);
return (
<Fragment>
<List
{...props}
className={classnames(classes.list, {[classes.listWithDrawer]: isMatch})}
sort={{ field: "name", order: "ASC" }}
>
<ReferenceManyField
{...props}
sort={{ field: "startTime", order: "DESC" }}
reference="TimesheetSummary"
source="employeeCode"
target="employeeCode"
label={false}
>
<Datagrid rowClick={details} {...props}>
<TextField
label="Timesheet ID"
source="timesheetID"
sortable={false}
/>
<FunctionField
label="Start Time"
source="startTime"
render={record => `${dateFormat(record.startTime)}`}
sortable={false}
/>
</Datagrid>
</ReferenceManyField>
</List>
<Drawer
variant="persistent"
open={isMatch}
anchor="right"
onClose={this.handleClose}
classes={{ paper: classes.drawerPaper }}
>
{/* To avoid any errors if the route does not match,
we don't render at all the component in this case */}
{ isMatch ? (
<TimesheetsummaryShow
{...props}
id={match.params.id}
onCancel={this.handleClose}
/>
) : null}
</Drawer>
</Fragment>
);
}}
</Route>
</div>
);
}
The Admin resource definition for this:
import TimesheetSummary from './timesheets/index';
<Resource name="TimesheetSummary" {...TimesheetSummary} />
Here is the timesheet index:
import TimesheetsummaryShow from './timesheetShow';
import TimesheetCreate from './TimesheetCreate';
export default {
create: TimesheetCreate,
show: TimesheetsummaryShow
};
I'm working with a nested state object that I have been updating with onChange functions, like so:
const [someState, setSomeState] = useState({
customer: [
{
name: "Bob",
address: "1234 Main Street",
email: "bob#mail.com",
phone: [
{
mobile: "555-5555",
home: "555-5555"
}
]
}
]
});
const updateSomeStatePhone = e => {
e.persist();
setSomeState(prevState => {
prevState.customer[0].phone[0].mobile = e.target.value;
return {
...prevState
};
});
};
<p>Update Mobile Number<p>
<select
value={someState.customer[0].phone[0].mobile}
onChange={updateSomeStatePhone}
>
<option value="123-4567">"123-4567"</option>
</select>
This gets the trick done. Currently however, if I want to update multiple state properties via a large form with dropdowns/input fields etc, I have to hard code 6 different onChange handlers for those fields.
Instead, I would prefer to have only one onChange handler, and pass in the state from the form field for the state property that I am changing, but I can't figure out the syntax:
const updateSomeState = (e, prop) => {
e.persist();
setSomeState(prevState => {
prevState.prop = e.target.value;
return {
...prevState
};
});
};
<p>Update Mobile Number<p>
<select
value={someState.customer[0].phone[0].mobile}
onChange={updateSomeState(e, prop)}
>
<option value="123-4567">"123-4567"</option>
</select>
I've tried using different types of syntax to chain the passed in 'prop' value to prevState:
prevState.prop = e.target.value;
prevState.(prop) = e.target.value;
${prevState} + '.' + ${prop} = e.target.value; // Dumb, I know
But the function never recognizes the "prop" that I pass in from the function. I'm sure there must be a simple way to do this. Any help would be greatly appreciated.
Does it have to be a single useState hook? I would recommend using useReducer or simplifying it a bit with multiple useState hooks.
Multiple useState hooks
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [name, setName] = React.useState("");
const [address, setAddress] = React.useState("");
const [email, setEmail] = React.useState("");
const [mobile, setMobile] = React.useState("");
const [home, setHome] = React.useState("");
const getResult = () => ({
customer: [
{
name,
address,
email,
phone: [
{
mobile,
home
}
]
}
]
});
// Do whatever you need to do with this
console.log(getResult());
return (
<>
<input
value={name}
placeholder="name"
onChange={e => setName(e.target.value)}
/>
<br />
<input
value={address}
placeholder="address"
onChange={e => setAddress(e.target.value)}
/>
<br />
<input
value={email}
placeholder="email"
onChange={e => setEmail(e.target.value)}
/>
<br />
<input
value={mobile}
placeholder="mobile"
onChange={e => setMobile(e.target.value)}
/>
<br />
<input
value={home}
placeholder="home"
onChange={e => setHome(e.target.value)}
/>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Single useReducer (with simplified state)
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const reducer = (state, action) => {
const { type, value } = action;
switch (type) {
case "SET_NAME":
return { ...state, name: value };
case "SET_ADDRESS":
return { ...state, address: value };
case "SET_EMAIL":
return { ...state, email: value };
case "SET_MOBILE":
return { ...state, phone: [{ ...state.phone[0], mobile: value }] };
case "SET_HOME":
return { ...state, phone: [{ ...state.phone[0], home: value }] };
default:
throw Error(`Unexpected action: ${action.type}`);
}
};
const initialState = {
name: "",
address: "",
email: "",
phone: [
{
mobile: "",
home: ""
}
]
};
function App() {
const [state, dispatch] = React.useReducer(reducer, initialState);
// Do what you need with state
console.log(state);
return (
<>
<input
value={state.name}
placeholder="name"
onChange={({ target: { value } }) =>
dispatch({ type: "SET_NAME", value })
}
/>
<br />
<input
value={state.address}
placeholder="address"
onChange={({ target: { value } }) =>
dispatch({ type: "SET_ADDRESS", value })
}
/>
<br />
<input
value={state.email}
placeholder="email"
onChange={({ target: { value } }) =>
dispatch({ type: "SET_EMAIL", value })
}
/>
<br />
<input
value={state.phone.mobile}
placeholder="mobile"
onChange={({ target: { value } }) =>
dispatch({ type: "SET_MOBILE", value })
}
/>
<br />
<input
value={state.phone.home}
placeholder="home"
onChange={({ target: { value } }) =>
dispatch({ type: "SET_HOME", value })
}
/>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useReducer is a better choice for doing this. Examples all over the internet.
Why you shouldn't use useState to pass an object is because it doesn't act like setState. The underlying object reference is the same. Therefore, react will never trigger a state change. In case you want to use the same useState for objects. You may have to implement your own version to extend that (example below ) or you can directly use useReducer hook to achieve the same.
Here's an example with useState for you to notice the state update on every change.
const [form, setValues] = useState({
username: "",
password: ""
});
const updateField = e => {
setValues({
...form,
[e.target.name]: e.target.value
});
};
Notice the ...form in there. You can do it this in every update you want or you can use your own utility or useReducer as I mentioned.
Now coming to your code, there are other concerns.
You are using your phone as an array which can be an object. Or better yet separate properties will do as well. No harm.
If you have customers as an array, you have to loop through the records. Not just update the index by hardcoding. If there's only one customer better not keep the array but just an object. Assuming it is an array of customers, and you are looping through it, here's how to update mobile.
const updatedCustomers = state.customers.map(item => {
const { phone } = item;
return { ...item, phone: { mobile: e.target.value }};
// returns newCustomer object with updated mobile property
});
// Then go ahead and call `setSomeState ` from `useState`
setSomeState(...someState, { customer: updatedCustomers });// newState in your case is
Instead, I would prefer to have only one onChange handler, and pass in
the state from the form field for the state property that I am
changing, but I can't figure out the syntax
If you haven't figured that out from the first example. Here's how in short steps.
Give your HTML element a name attribute.
Then instead use the [e.target.name]
return { ...item, phone: { [e.target.name]: e.target.value }};
Use lodash's _.set helper.
const updateSomeState = (e, prop) => {
e.persist();
setSomeState(prevState => {
let customers = [...prevState.customers] // make a copy of array
let customer = {...customers[0]} // make a copy of customer object
_.set(customer, prop, e.target.value)
customers[0] = customer;
return {
...prevState, customers
};
});
};
BTW, in your existing updateSomeStatePhone you are modifying prevState object which is supposed to be immutable.
I have set of components where it would consist of input fields along with text rows.
As given in the image the users should be able to add categories and description. After adding them they will be rendered as a list of components. like this
Inside a category there will be tags as given in the above image and to add them i have to add a input component. This input component should be available only when the user clicks on the Add tag button below each category row. When a user clicks on it,it should enable the input(should render a input component inside the selected category row) and should be able to type the tag name on it and save it. I need to make this input field enable only when i click on the add tag button. and it should enable only in the selected category row. This is the code that i have tried.
import React, { Component, Fragment } from "react";
import { Button, Header, Input } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
import ReactDOM from "react-dom";
class App extends Component {
state = {
category: "",
description: "",
categories: []
};
onChange = (e, { name, value }) => {
this.setState({ [name]: value });
};
addCategory = () => {
let { category, description } = this.state;
this.setState(prevState => ({
categories: [
...prevState.categories,
{
id: Math.random(),
title: category,
description: description,
tags: []
}
]
}));
};
addTag = id => {
let { tag, categories } = this.state;
let category = categories.find(cat => cat.id === id);
let index = categories.findIndex(cat => cat.id === id);
category.tags = [...category.tags, { name: tag }];
this.setState({
categories: [
...categories.slice(0, index),
category,
...categories.slice(++index)
]
});
};
onKeyDown = e => {
if (e.key === "Enter" && !e.shiftKey) {
console.log(e.target.value);
}
};
tags = tags => {
if (tags && tags.length > 0) {
return tags.map((tag, i) => {
return <Header key={i}>{tag.name}</Header>;
});
}
};
enableTagIn = id => {};
categories = () => {
let { categories } = this.state;
return categories.map(cat => {
return (
<Fragment key={cat.id}>
<Header>
<p>
{cat.title}
<br />
{cat.description}
</p>
</Header>
<Input
name="tag"
onKeyDown={e => {
this.onKeyDown(e);
}}
onChange={this.onChange}
/>
<Button
onClick={e => {
this.addTag(cat.id);
}}
>
Add
</Button>
{this.tags(cat.tags)}
</Fragment>
);
});
};
render() {
return (
<Fragment>
{this.categories()}
<div>
<Input name="category" onChange={this.onChange} />
<Input name="description" onChange={this.onChange} />
<Button onClick={this.addCategory}>Save</Button>
</div>
</Fragment>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
This is the codesandbox url.
Any idea on how to achieve this?.
I changed your code by using function components and react hooks and i created category component which has it own state like this:
import React, { Fragment } from "react";
import { Button, Header, Input } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
import ReactDOM from "react-dom";
const App = () => {
const [Category, setCategory] = React.useState({
title: "",
description: ""
});
const [Categories, setCategories] = React.useState([]);
return (
<div>
{console.log(Categories)}
<Input
value={Category.title}
onChange={e => setCategory({ ...Category, title: e.target.value })}
/>
<Input
value={Category.description}
onChange={e =>
setCategory({ ...Category, description: e.target.value })
}
/>
<Button onClick={() => setCategories([...Categories, Category])}>
Save
</Button>
<div>
{Categories.length > 0
? Categories.map(cat => <CategoryItem cat={cat} />)
: null}
</div>
</div>
);
};
const CategoryItem = ({ cat }) => {
const [value, setvalue] = React.useState("");
const [tag, addtag] = React.useState([]);
const [clicked, setclicked] = React.useState(false);
const add = () => {
setclicked(false);
addtag([...tag, value]);
};
return (
<Fragment>
<Header>
<p>
{cat.title}
<br />
{cat.description}
</p>
</Header>
<Input
name="tag"
value={value}
style={{ display: clicked ? "initial" : "none" }}
onChange={e => setvalue(e.target.value)}
/>
<Button onClick={() => (clicked ? add() : setclicked(true))}>Add</Button>
<div>{tag.length > 0 ? tag.map(tagname => <p>{tagname}</p>) : null}</div>
</Fragment>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
and here a sandbox
I have this json object which I have taken from the news API and want to print out the author of just one of the articles. I wanted to know how to do it within a react component which I have called 'author'.
Here is the json object: it's too long to include here but have the link for you to see.
It's accessible from https://newsapi.org/ and has a total of 20 articles.
I have this react component which I am trying to then print one of the article's authors:
import React, { Component } from 'react';
const APIurl = 'https://newsapi.org/v2/top-headlines?
country=it&apiKey=0b3e87958d0b4e71a9e2ed3eea69237a';
class Author extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
fetch(APIurl)
.then(response => response.json())
.then(response => {
this.setState({
articles: response
})
})
}
render() {
return (
<h5 class="f6 ttu tracked black-80">
{this.state.articles.article[0].author}
</h5>
);
}
}
export default Author;
However, something must not be quite right because I get this error:
TypeError: Cannot read property 'articles' of undefined
21 | render() {
22 | return (
23 | <h5 class="f6 ttu tracked black-80">
> 24 | {this.state.articles.articles[0].author}
25 | </h5>
26 | );
27 | }
I'm not sure what I have done wrong. Also sorry for the poor formating of the json object.
I've now made some changes after seeing what has been suggested below so that my code looks like this:
import React, { Component } from 'react';
const APIurl = 'https://newsapi.org/v2/top-headlines? country=it&apiKey=0b3e87958d0b4e71a9e2ed3eea69237a';
class Author extends Component {
constructor(props) {
super(props);
this.state = {
articles: []
};
}
componentDidMount() {
fetch(APIurl)
.then(response => response.json())
.then(response => {
this.setState({
articles: response
})
})
}
render() {
const { articles } = this.state;
return (
<h5 class="f6 ttu tracked black-80">
{articles.length>0 && articles.articles[1].author}
</h5>
);
}
}
export default Author;
However, it still doesn't print out anything in the author react component even though when I go to the chrome developer tools and see the state of the component it looks like this:
State
articles: {…}
articles: Array[20]
0: {…}
1: {…}
author: "Davide Stoppini"
description: "A Pisa, divertente pareggio con i russi, più avanti per quanto riguarda la condizione fisica. Passi in avanti rispetto al Sion: bene gli esterni offensivi, anche i due attaccanti confermano la confide…"
publishedAt: "2018-07-21T20:20:21Z"
source: {…}
title: "Inter, fuochi d'artificio con lo Zenit: è 3-3. In gol Icardi e Lautaro"
url: "https://www.gazzetta.it/Calcio/Serie-A/Inter/21-07-2018/inter-fuochi-d-artificio-lo-zenit-3-3-gol-icardi-lautaro-280799153444.shtml"
urlToImage:"https://images2.gazzettaobjects.it/methode_image/2018/07/21/Calcio/Foto%20Calcio%20-%20Trattate/1d50f03c94d965c2ca84bd3eec0137c9_169_xl.jpg
*Note: this is only showing the first second element of the articles array.
Basically, you have to declare articles as empty array initially as follows:
this.state = {
articles: []
};
And also need to modify your code inside render as follows:
{this.state.articles && (this.state.articles.article.length>0) &&
this.state.articles.article[0].author
}
Hope it will help you.
The problem you are having is because your code is not prepared to go through the lifecycle of React. Before you get the data in the componentDidMount phase there is a render phase, that is where the error is happening. In that render phase articles should be an empty array without data and then you need a check to avoid rendering any stuff if the array is empty. So to avoid to have that error in the render phase before the componentDidMount phase you need to set in state an empty array called articles, and in the render method to check if it is not empty, to render the value you want.
import React, { Component } from 'react';
const APIurl = 'https://newsapi.org/v2/top-headlines?
country=it&apiKey=0b3e87958d0b4e71a9e2ed3eea69237a';
class Author extends Component {
constructor(props) {
super(props);
this.state = { articles: []};
}
componentDidMount() {
fetch(APIurl)
.then(response => response.json())
.then(response => {
this.setState({
articles: response.articles
})
})
}
render() {
const { articles } = this.state;
return (
<h5 class="f6 ttu tracked black-80">
{articles.length > 0 && articles[0].author}
</h5>
);
}
}
export default Author;
News App in React Native:
const SITE_URL =
"https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=a39bbc7131c649a3ad23fe79063d996f";
const TestScreen = () => {
const [myData, setMyData] = useState();
useEffect(() => {
axios
.get(SITE_URL)
.then((res) => {
// console.log("Response from main API: ", res);
console.log(
"----------------------------------------------------------- Start"
);
// console.log("Response from Home Data Data: ", res.data.data);
console.log(
"Response from NEWS data articles: ",
res.data.articles
);
console.log(
"----------------------------------------------------------- End"
);
// let companyData = res.data.data;
let companyData = res.data.articles;
setMyData(companyData);
setData({
Company: companyData,
Description: "",
});
})
.catch((err) => {
console.log(err);
});
}, []);
// console.log("myData:", { myData });
const renderItem = ({ item, index }) => (
<TouchableOpacity style={styles.container}>
<Text style={styles.title}>
{index}. {item.author}
</Text>
<Text> {item.description} </Text>
<View>
<Image
style={{
width: 500,
height: 100,
}}
source={{
uri: item.urlToImage,
}}
/>
</View>
</TouchableOpacity>
);
return (
<View style={styles.container}>
<Text>Read Later Screen</Text>
<Text>|</Text>
{<FlatList data={myData} renderItem={renderItem} />}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
export default TestScreen;