Related
Here I'm working on AutoComplete and Auto fill of react.
I'm trying to convert it to react hooks as I have written all of my code is in hooks only.
I've to some level converted it to hooks based as per my understanding. But I'm not able to completely convert it.
Original code
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.state = {
item: {
code: "",
name: "",
unit: "",
rate: ""
},
cursor: 0,
searchItems: []
};
this.autocomplete = this.autocomplete.bind(this);
this.handleKeyup = this.handleKeyup.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
this.handleListKeydown = this.handleListKeydown.bind(this);
this.selectItem = this.selectItem.bind(this);
this.handleChange = this.handleChange.bind(this);
}
autocomplete(evt) {
let text = evt.target.value;
fetch(`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`)
.then((res) => res.json())
.then((data) => {
this.setState({ searchItems: data });
});
}
handleKeyup(evt) {
if (evt.keyCode === 27) {
this.setState({ searchItems: [] });
return false;
}
}
handleKeydown(evt) {
const { cursor, searchItems } = this.state;
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
this.setState((prevState) => ({
cursor: prevState.cursor - 1
}));
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
this.setState((prevState) => ({
cursor: prevState.cursor + 1
}));
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const { name, code, rate, unit } = currentItem;
this.setState({ item: { name, code, rate, unit }, searchItems: [] });
}
}
if (evt.keyCode === 8) {
this.setState({ item: { name: "", code: "", rate: "", unit: "" } });
}
}
selectItem(id) {
const { searchItems } = this.state;
let selectedItem = searchItems.find((item) => item.code === id);
const { code, name, unit, rate } = selectedItem;
this.setState({ item: { code, name, unit, rate } });
this.setState({ searchItems: [] });
}
handleListKeydown(evt) {
console.log(evt.keyCode);
}
handleChange(evt) {
this.setState({ item: { [evt.target.name]: evt.target.value } });
}
render() {
const { searchItems, cursor, item, handleChange } = this.state;
const { code, name, unit, rate } = item;
return (
<div className="container mt-3">
<h1 className="h2 text-center">Autocomplete Example</h1>
<div className="form-group">
<label htmlFor="autocomplete">Item Name </label>
<input
type="text"
id="autocomplete"
onChange={this.autocomplete}
onKeyUp={this.handleKeyup}
onKeyDown={this.handleKeydown}
value={name}
className="custom-input form-control"
/>
{searchItems.length > 0 && (
<ul className="list-group">
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? "active list-group-item"
: "list-group-item"
}
key={idx}
onClick={() => this.selectItem(item.code)}
onKeyDown={(evt) => this.handleListKeydown(evt, item.code)}
>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
}
export default App;
Link to original code: https://codepen.io/regexp/details/RwPNaLe
Using hooks
Here is the code that I tried to convert to hooks.
import React, { useState } from "react";
export default function FunctionName(props) {
const [item, setItem] = useState({
vendorNameData: invoiceDetail[0].invoiceData.vendor,
vendorAccountData: invoiceDetail[0].invoiceData.vendaAccount,
vendorAddressData: invoiceDetail[0].invoiceData.vendorAddress
});
const [cursor, setCursor] = useState(0);
const [searchItems, SetSearchItems] = useState([]);
function AutoComplete(evt) {
let text = evt.target.value;
console.log(text);
fetch(`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`)
.then((res) => res.json())
.then((data) => {
SetSearchItems(data);
});
}
function HandleKeyUp(evt) {
if (evt.keyCode === 27) {
SetSearchItems([]);
return false;
}
}
function HandleKeyDown(evt) {
// const [cursor, setCursor] = useState();
// const [searchItems, SetSearchItems] = useState()
if (evt.keyCode === 38 && cursor > 0) {
setCursor((cursor) => ({ cursor: cursor + 1 }));
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((cursor) => ({ cursor: cursor + 1 }));
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {
vendorNameData,
vendorAccountData,
vendorAddressData
} = currentItem;
setItem({ vendorNameData, vendorAccountData, vendorAddressData });
SetSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({
vendorNameData: "",
vendorAccountData: "",
vendorAddressData: ""
});
}
}
function SelectItem(id) {
const [searchItems, SetSearchItems] = useState();
let selectedItem = searchItems.find((item) => item.code === id);
const {
vendorNameData,
vendorAccountData,
vendorAddressData
} = selectedItem;
setItem({ vendorNameData, vendorAccountData, vendorAddressData });
SetSearchItems([]);
}
function HandleListKeyDown(evt) {
console.log(evt.keyCode);
}
function HandleChange(evt) {
setItem({ item: { [evt.target.name]: evt.target.value } });
}
}
It would be really helpful to point me out where I'm lagging. I've tried my best but this is what I could come up with.
Any help would really be appreciated.
I've 'traduced' and cleaned up your original component, resulting as follows: (please see notes below)
import React, {useState, useCallback} from 'react';
function Input() {
const [item, setItem] = useState({
code: '',
name: '',
unit: '',
rate: ''
});
const [cursor, setCursor] = useState(0);
const [searchItems, setSearchItems] = useState([]);
const autocomplete = useCallback((evt) => {
const text = evt.target.value;
fetch(
`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`
)
.then((res) => res.json())
.then((data) => {
setSearchItems(data);
});
}, []);
const handleKeyup = useCallback((evt) => {
if (evt.keyCode === 27) {
setSearchItems([]);
return false;
}
return true;
}, []);
const handleKeydown = useCallback(
(evt) => {
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
setCursor((prevCursor) => prevCursor - 1);
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((prevCursor) => prevCursor + 1);
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {code, name, unit, rate} = currentItem;
setItem({code, name, unit, rate});
setSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({code: '', name: '', unit: '', rate: ''});
}
},
[cursor, searchItems]
);
const selectItem = useCallback(
(id) => {
let selectedItem = searchItems.find((item) => item.code === id);
const {code, name, unit, rate} = selectedItem;
setItem({code, name, unit, rate});
setSearchItems([]);
},
[searchItems]
);
const handleListKeydown = useCallback((evt) => {
console.log(evt.keyCode);
}, []);
return (
<div className={'container mt-3'}>
<h1 className={'h2 text-center'}>{'Autocomplete Example'}</h1>
<div className={'form-group'}>
<label htmlFor={'autocomplete'}>{'Item Name'}</label>
<input
type={'text'}
id={'autocomplete'}
onChange={autocomplete}
onKeyUp={handleKeyup}
onKeyDown={handleKeydown}
value={item.name}
className={'custom-input form-control'}
/>
{searchItems.length > 0 && (
<ul className={'list-group'}>
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? 'active list-group-item'
: 'list-group-item'
}
key={idx}
onClick={() => selectItem(item.code)}
onKeyDown={handleListKeydown}>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
export {Input};
useState replaces Class Components State management. It is good practice to split your state into smaller pieces as possible, because new values will completely replace old ones (there is no merging like Class Components this.setState does). Using cursor as an example, do const [cursor, setCursor] = useState(0); to initialize your state (in this case initial state is 0). Then use cursor to use the value and call setCursor(newValue) to update it. Each time you update your state you will most likely trigger a re-rendering.
useCallback allows you to only re-declare a function when its dependencies have changed, thus improving performance. Function dependencies are specified in the second argument array. On each render, React will compare the new values with the old ones and will always return the same function when dependencies have not changed.
The return statement replaces your previous render method.
Follows a working snippet. Please note that OP original code behavior has not been changed.
const {useState, useCallback, StrictMode} = React;
function Input() {
const [item, setItem] = useState({
code: '',
name: '',
unit: '',
rate: ''
});
const [cursor, setCursor] = useState(0);
const [searchItems, setSearchItems] = useState([]);
const autocomplete = useCallback((evt) => {
const text = evt.target.value;
fetch(
`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`
)
.then((res) => res.json())
.then((data) => {
setSearchItems(data);
});
}, []);
const handleKeyup = useCallback((evt) => {
if (evt.keyCode === 27) {
setSearchItems([]);
return false;
}
return true;
}, []);
const handleKeydown = useCallback(
(evt) => {
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
setCursor((prevCursor) => prevCursor - 1);
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((prevCursor) => prevCursor + 1);
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {code, name, unit, rate} = currentItem;
setItem({code, name, unit, rate});
setSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({code: '', name: '', unit: '', rate: ''});
}
},
[cursor, searchItems]
);
const selectItem = useCallback(
(id) => {
let selectedItem = searchItems.find((item) => item.code === id);
const {code, name, unit, rate} = selectedItem;
setItem({code, name, unit, rate});
setSearchItems([]);
},
[searchItems]
);
const handleListKeydown = useCallback((evt) => {
console.log(evt.keyCode);
}, []);
return (
<div className={'container mt-3'}>
<h1 className={'h2 text-center'}>{'Autocomplete Example'}</h1>
<div className={'form-group'}>
<label htmlFor={'autocomplete'}>{'Item Name'}</label>
<input
type={'text'}
id={'autocomplete'}
onChange={autocomplete}
onKeyUp={handleKeyup}
onKeyDown={handleKeydown}
value={item.name}
className={'custom-input form-control'}
/>
{searchItems.length > 0 && (
<ul className={'list-group'}>
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? 'active list-group-item'
: 'list-group-item'
}
key={idx}
onClick={() => selectItem(item.code)}
onKeyDown={handleListKeydown}>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
ReactDOM.render(
<StrictMode>
<Input />
</StrictMode>,
document.getElementById('root')
);
.active {
color: #ff0000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
<div id='root' />
I have a class that loads in data during construction and sets the initial state of two variables, itemsPrayer, and itemsNews. This works fine. Then I have a getData() method where I call the same firebase firestore collections and I use onSnapShot() so I can receive instant changes.
The itemsPrayer works just fine, as soon as I type a new message it gets output to the window, but when I type a new message for itemsNews, I have to refresh the component manually in order to see the new comment.
My thought is, am I able to have two onSnapShots()?
export class Landing extends Component {
constructor(props) {
super(props);
this.state = {
tab: 0,
isDesktop: false,
form: false,
bottom: false,
viewPopup: true,
itemsPrayer: Array.from(
db.collection("prayerRequest").orderBy("created", "asc").get()
),
itemsNews: Array.from(
db.collection("upliftingStories").orderBy("created", "asc").get()
),
};
this.updatePredicate = this.updatePredicate.bind(this);
// this.getData = this.getData.bind(this);
}
// componentDidUpdate() {
// this.getData();
// }
getData = () => {
db.collection("prayerRequest")
.orderBy("created", "asc")
.onSnapshot((snapshot) => {
let chat = this.state.itemsPrayer;
let changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
chat.push({ ...change.doc.data() });
}
});
this.setState({ itemsPrayer: chat });
});
db.collection("upliftingStories")
.orderBy("created", "asc")
.onSnapshot((snapshot) => {
let chat = this.state.itemsNews;
let changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
chat.push({ ...change.doc.data() });
}
});
this.setState({ itemsNews: chat });
});
};
componentDidMount() {
this.getData();
this.updatePredicate();
window.addEventListener("resize", this.updatePredicate);
if (window.location.reload) {
this.setState({ viewPopup: true });
setTimeout(
function () {
this.setState({ viewPopup: false });
}.bind(this),
8000
);
}
}
componentWillUnmount() {
window.removeEventListener("resize", this.updatePredicate);
}
updatePredicate() {
this.setState({ isDesktop: window.innerWidth > 410 });
}
togglePopup() {
this.setState({ form: true });
}
render() {
const { classes } = this.props;
const isDesktop = this.state.isDesktop;
const handleChange = (event, newValue) => {
this.setState({ tab: newValue });
};
let window = this.state.tab ? (
<WindowUplifting items={this.state.itemsNews} />
) : (
<WindowPrayerRequest items={this.state.itemsPrayer} />
);
return (
<div className={classes.root}>
{isDesktop ? (
<Tabs
value={this.state.tab}
onChange={handleChange}
variant="fullWidth"
aria-label="simple tabs example"
className={classes.tabs}
style={{ backgroundColor: "#000000" }}
>
<Tab style={{ color: "#ffffff" }} label="Prayer Requests" />
<Tab style={{ color: "#ffffff" }} label="Uplifting Stories" />
</Tabs>
........
I tried this code based on a comment but the same thing happens,
componentDidMount() {
const prayer_query = db
.collection("prayerRequest")
.orderBy("created", "asc");
const news_query = db
.collection("upliftingStories")
.orderBy("created", "asc");
//
prayer_query.onSnapshot((snapshot) => {
let chat = this.state.itemsPrayer;
let changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
chat.push({ ...change.doc.data() });
}
});
this.setState({ itemsPrayer: chat });
});
news_query.onSnapshot((snapshot) => {
let chat = this.state.itemsNews;
let changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
chat.push({ ...change.doc.data() });
}
});
this.setState({ itemsNews: chat });
});
onSnapshot() works like an event listener for changes to firestore documents. Typically, you will put these methods in the componentDidMount method of a React component instead of putting them as a function on its own, to get the intended result you achieve.
You can add snippets of onSnapshot() in a format like this.
//Initialise change object
let new_change_object = {};
//Listeners
prayer_query.onSnapshot((snapshot) => {
let chat = this.state.itemsPrayer;
let changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
chat.push({ ...change.doc.data() });
}
});
new_change_object["itemsPrayer"] = chat;
});
news_query.onSnapshot((snapshot) => {
let chat = this.state.itemsNews;
let changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
chat.push({ ...change.doc.data() });
}
});
new_change_object["itemsNews"] = chat;
});
this.setState(new_change_object);
I am in need of an accessible tooltip component in React and came across React Tooltip. Its written in flow but I use jsx in my build. I want to convert the syntax to jsx but hitting a roadblock in the render method. Please advice on what I need to change.
Flow Tooltip: https://pastebin.com/EfQQBKZ0
// #flow
import React, { Component } from 'react';
import type { ElementProps, ComponentType } from 'react';
export type LabelProps = {
labelAttributes: {
tabIndex: '0',
'aria-describedby': string,
onFocus: () => void,
},
isHidden: boolean,
};
export type OverlayProps = {
overlayAttributes: {
role: 'tooltip',
tabIndex: '-1',
id: string,
'aria-hidden': string,
},
isHidden: boolean,
};
export type TooltipState = {
isFocused: boolean,
isHovered: boolean,
};
export type TooltipProps = ElementProps<'div'> & {
label: ComponentType<LabelProps>,
overlay: ComponentType<OverlayProps>,
};
let counter = 0;
class Tooltip extends Component<TooltipProps, TooltipState> {
constructor(props: TooltipProps) {
super(props);
this.identifier = `react-accessible-tooltip-${counter}`;
counter += 1;
}
state = {
isFocused: false,
isHovered: false,
};
componentDidMount() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('touchstart', this.handleTouch);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('touchstart', this.handleTouch);
}
onFocus = () => {
this.setState({ isFocused: true });
};
onBlur = ({
relatedTarget,
currentTarget,
}: SyntheticFocusEvent<HTMLElement>) => {
// relatedTarget is better for React testability etc, but activeElement works as an IE11 fallback:
const newTarget = relatedTarget || document.activeElement;
// The idea of this logic is that we should only close the tooltip if focus has shifted from the tooltip AND all of its descendents.
if (!(newTarget && newTarget instanceof HTMLElement)) {
this.setState({ isFocused: false });
} else if (!currentTarget.contains(newTarget)) {
this.setState({ isFocused: false });
}
};
onMouseEnter = () => {
this.setState({ isHovered: true });
};
onMouseLeave = () => {
this.setState({ isHovered: false });
};
// This handles the support for touch devices that do not trigger blur on 'touch-away'.
handleTouch = ({ target }: Event) => {
const { activeElement } = document;
if (
activeElement instanceof Element &&
target instanceof Element &&
this.container instanceof Element &&
!this.container.contains(target) && // touch target not a tooltip descendent
this.state.isFocused // prevent redundant state change
) {
this.setState({ isFocused: false });
activeElement.blur();
} else if (
activeElement instanceof Element &&
target instanceof Element &&
this.container instanceof Element &&
this.container.contains(target) && // touch target is on tooltip descendant
!this.state.isFocused // prevent redundant state change
) {
this.setState({ isFocused: true });
}
};
handleKeyDown = ({ key, keyCode, which }: KeyboardEvent) => {
if (key === 'Escape' || keyCode === 27 || which === 27) {
this.setState({ isFocused: false });
}
};
container: ?HTMLDivElement;
identifier: string;
render() {
const { label: Label, overlay: Overlay, ...rest } = this.props;
const { isFocused, isHovered } = this.state;
const isHidden = !(isFocused || isHovered);
const labelProps: LabelProps = {
labelAttributes: {
tabIndex: '0',
'aria-describedby': this.identifier,
onFocus: this.onFocus,
},
isHidden,
};
const overlayProps: OverlayProps = {
overlayAttributes: {
role: 'tooltip',
tabIndex: '-1',
id: this.identifier,
'aria-hidden': isHidden.toString(),
},
isHidden,
};
return (
<div
{...rest}
onBlur={this.onBlur}
ref={ref => {
this.container = ref;
}}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<Label {...labelProps} />
<Overlay {...overlayProps} />
</div>
);
}
}
export default Tooltip;
JSX Tooltip: https://pastebin.com/QhQqRw24
import React, { Component } from 'react';
import { faInfoCircle } from '#fortawesome/free-solid-svg-icons';
import InfoIcon from '../../InfoIcon/InfoIcon';
let counter = 0;
class TooltipContainer extends Component {
constructor(props) {
super(props);
this.state = {
isFocused: false,
isHovered: false,
};
this.identifier = `react-accessible-tooltip-${counter}`;
counter += 1;
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onMouseEnter = this.onMouseEnter.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
this.handleTouch = this.handleTouch.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.container = React.createRef();
}
componentDidMount() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('touchstart', this.handleTouch);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('touchstart', this.handleTouch);
}
onFocus() {
this.setState({ isFocused: true });
};
onBlur({
relatedTarget,
currentTarget,
}){
// relatedTarget is better for React testability etc, but activeElement works as an IE11 fallback:
const newTarget = relatedTarget || document.activeElement;
// The idea of this logic is that we should only close the tooltip if focus has shifted from the tooltip AND all of its descendents.
if (!(newTarget)) {
this.setState({ isFocused: false });
} else if (!currentTarget.contains(newTarget)) {
this.setState({ isFocused: false });
}
}
onMouseEnter() {
this.setState({ isHovered: true });
}
onMouseLeave() {
this.setState({ isHovered: false });
}
// This handles the support for touch devices that do not trigger blur on 'touch-away'.
handleTouch({ target }) {
const { activeElement } = document;
if (
activeElement instanceof Element &&
target instanceof Element &&
this.container instanceof Element &&
!this.container.contains(target) && // touch target not a tooltip descendent
this.state.isFocused // prevent redundant state change
) {
this.setState({ isFocused: false });
activeElement.blur();
} else if (
activeElement instanceof Element &&
target instanceof Element &&
this.container instanceof Element &&
this.container.contains(target) && // touch target is on tooltip descendant
!this.state.isFocused // prevent redundant state change
) {
this.setState({ isFocused: true });
}
}
handleKeyDown({ key, keyCode, which }) {
if (key === 'Escape' || keyCode === 27 || which === 27) {
this.setState({ isFocused: false });
}
}
render() {
console.log('props', this.props)
const { label: Label, overlay: Overlay, ...rest } = this.props;
const { isFocused, isHovered } = this.state;
const isHidden = !(isFocused || isHovered);
const labelProps = {
labelAttributes: {
tabIndex: '0',
'aria-describedby': this.identifier,
onFocus: this.onFocus,
},
isHidden,
};
const overlayProps = {
overlayAttributes: {
role: 'tooltip',
tabIndex: '-1',
id: this.identifier,
'aria-hidden': isHidden.toString(),
},
isHidden,
};
return (
<div
{...rest}
onBlur={this.onBlur}
ref={ref => {
this.container = ref;
}}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<a><InfoIcon icon={faInfoCircle} {...labelProps} /></a>
<Overlay {...overlayProps} />
</div>
);
}
}
export default TooltipContainer;
I just can not get, why you don't want to just install that package and use it following API description.
Related to your question, Flow it's just a type checker, if you want to get rid of it, just remove flow-related things from the code.
I see that you did some not related to flow changes, for example, refs. In your example with the flow, there is a callback refs, but in your example without, you're trying to use new refs API, but still using it as a callback refs. That's from the first look, mb there is more other things...
Plus
roadblock in the render
doesn't sound informative, what issue you have faced.
I found a lot of solutions about this problem but none of them work.
I have a view which renders dynamically components depending on the backend response
/**
* Module dependencies
*/
const React = require('react');
const Head = require('react-declarative-head');
const MY_COMPONENTS = {
text: require('../components/fields/Description'),
initiatives: require('../components/fields/Dropdown'),
vuln: require('../components/fields/Dropdown'),
severities: require('../components/fields/Dropdown'),
};
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class CreateView extends React.Component {
constructor(props) {
super(props);
this.state = {
modal: false,
states: props.states,
error: props.error,
spinner: true,
state: props.state,
prevState: '',
components: [],
};
this.handleChange = this.handleChange.bind(this);
this.getRequiredFields = this.getRequiredFields.bind(this);
this.onChangeHandler = this.onChangeHandler.bind(this);
this.changeState = this.changeState.bind(this);
this.loadComponents = this.loadComponents.bind(this);
}
componentDidMount() {
this.loadComponents();
}
onChangeHandler(event, value) {
this.setState((prevState) => {
prevState.prevState = prevState.state;
prevState.state = value;
prevState.spinner = true;
return prevState;
}, () => {
this.getRequiredFields();
});
}
getRequiredFields() {
request.get('/transitions/fields', {
params: {
to: this.state.state,
from: this.state.prevState,
},
})
.then((response) => {
const pComponents = this.state.components.map(c => Object.assign({}, c));
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
this.setState({
components: pComponents,
fields: response.data,
spinner: false,
});
})
.catch(err => err);
}
loadComponents() {
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
}
handleChange(field, value) {
this.setState((prevState) => {
prevState[field] = value;
return prevState;
});
}
changeState(field, value) {
this.setState((prevState) => {
prevState[`${field}`] = value;
return prevState;
});
}
render() {
const Components = this.state.components;
return (
<Page name="CI" state={this.props} Components={Components}>
<Script src="vendor.js" />
<Card className="">
<div className="">
<div className="">
<Spinner
show={this.state.spinner}
/>
{Components.map((component, i) => {
const Comp = component.component;
return (<Comp
key={i}
value={this.state[component.field.name]}
field={component.field}
handleChange={this.handleChange}
modal={this.state.modal}
changeState={this.changeState}
/>);
})
}
</div>
</div>
</div>
</Card>
</Page>
);
}
}
module.exports = CreateView;
and the dropdown component
const React = require('react');
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class DrpDwn extends React.Component {
constructor(props) {
super(props);
this.state = {
field: props.field,
values: [],
};
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('state', this.state.field);
console.log('prevState', prevState.field);
console.log('prevProps', prevProps.field);
console.log('props', this.props.field);
}
render() {
const { show } = this.props.field;
return (show && (
<div className="">
<Dropdown
className=""
onChange={(e, v) => this.props.handleChange(this.props.field.name, v)}
label={this.state.field.name.replace(/^./,
str => str.toUpperCase())}
name={this.state.field.name}
type="form"
value={this.props.value}
width={100}
position
>
{this.state.values.map(value => (<DropdownItem
key={value.id}
value={value.name}
primary={value.name.replace(/^./, str => str.toUpperCase())}
/>))
}
</Dropdown>
</div>
));
}
module.exports = DrpDwn;
The code actually works, it hide or show the components correctly but the thing is that i can't do anything inside componentdidupdate because the prevProps prevState and props are always the same.
I think the problem is that I'm mutating always the same object, but I could not find the way to do it.
What I have to do there is to fill the dropdown item.
Ps: The "real" code works, i adapt it in order to post it here.
React state is supposed to be immutable. Since you're mutating state, you break the ability to tell whether the state has changed. In particular, i think this is the main spot causing your problem:
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
}; return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
You mutate the previous states to changes its components property. Instead, create a new state:
this.setState(prevState => {
const components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return { components }
}
You have an additional place where you're mutating state. I don't know if it's causing your particular problem, but it's worth mentioning anyway:
const pComponents = [].concat(this.state.components);
// const pComponents = [...this.state.components];
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
You do at make a copy of state.components, but this will only be a shallow copy. The array is a new array, but the objects inside the array are the old objects. So when you set ob.field.required, you are mutating the old state as well as the new.
If you want to change properties in the objects, you need to copy those objects at every level you're making a change. The spread syntax is usually the most succinct way to do this:
let pComponents = this.state.components.map(c => {
return {
...c,
field: {
...c.field,
required: 0,
show: false
}
}
});
response.data.forEach(r => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
// Here it's ok to mutate, but only because i already did the copying in the code above
ob.field.required = r.required;
ob.field.show = true;
}
})
I am just getting acquainted with testing and I have a little problem. I'm trying to test the functions of a component that change state and call functions of a document. In the documentation of Jest and Enzyme, I did not find the right example. Here is a sample code:
class EditableText extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
isEditing: false,
currentValue: null
};
this.textAreaRef = React.createRef();
}
onClick = () => {
if (!this.state.isEditing) {
this.setState({ isEditing: true });
setTimeout(() => {
this.textAreaRef.current.focus();
}, 100);
document.addEventListener('keydown', this.onKeyDown);
document.addEventListener('click', this.onOutsideClick);
}
};
onOutsideClick = e => {
if (e.target.value !== this.state.currentValue) {
this.closeEditMode();
}
};
closeEditMode() {
this.setState({ isEditing: false });
document.removeEventListener('keydown', this.onKeyDown);
document.removeEventListener('click', this.onOutsideClick);
}
onKeyDown = e => {
if (e.code === 'Enter') {
this.onUpdateValue();
} else if (e.code === 'Escape') {
this.closeEditMode();
}
};
onUpdateValue = () => {
this.props.onSubmit(this.state.currentValue || this.props.value);
this.closeEditMode();
};
render() {
const { isEditing, currentValue } = this.state;
const { value } = this.props;
return isEditing ? (
<TextArea
className="editable-text__textarea"
ref={this.textAreaRef}
onClick={this.onClick}
value={currentValue === null ? value || '' : currentValue}
onChange={(_e, { value }) => this.setState({ currentValue: value })}
/>
) : (
<div className="editable-text__readonly" onClick={this.onClick}>
<div>{this.props.children}</div>
<div className="editable-text__icon-container">
<Icon name="edit" className="editable-text__icon" />
</div>
</div>
);
}
}
How to test functions, that receive and change state and calls document.addEventListener? Please, help.
UPDATE > Tests i already wrote:
describe('tests of component', () => {
test('Component renders correctly with isEditing=false', () => {
const component = renderer
.create(<EditableText children="I'm text in EditableText" />)
.toJSON();
expect(component).toMatchSnapshot();
});
test('Component changes to isEditing=true when clicked on it', () => {
const component = renderer
.create(<EditableText />);
let tree = component.toJSON();
tree.props.onClick();
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('tests of logic', () => {
test.only('OnClick should change state and add event listeners', ()=> {
const state = { isEditing: false, currentValue: null }
global.document.addEventListener = jest.fn();
EditableText.onClick(() => {
expect(global.document.addEventListener).toHaveBeenCalled();
});;
});
});