I have a react.js component where I want to pass down a bunch of different methods to child components from the parent component, the methods modify the state of the parent component.
class Page extends Component {
constructor(props) {
super(props);
this.state = {
items: this.props.items,
currentItemID: 1
};
this.actions = this.actions.bind(this);
}
render() {
return (
<div className="page">
{
this.state.items.map(item =>
<Item key={item._id} item={item} actions={this.actions} />
)
}
</div>
);
}
actions() {
return {
insertItem: function (itemID) {
const currentItems = this.state.items;
const itemPosition = this.state.items.map((item) => item._id).indexOf(itemID);
const blankItem = {
_id: (new Date().getTime()),
content: ''
};
currentItems.splice(itemPosition + 1, 0, blankItem)
this.setState({
items: currentItems,
lastAddedItemID: blankItem._id
});
},
setCurrentItem: function (itemID) {
this.setState({ currentItemID: itemID });
},
focus: function(itemID) {
return (itemID === this.state.currentItemID);
}
}
}
In my child component, I am trying to use the focus method in the componentDidMount lifecyle method as shown below:
componentDidMount() {
if (this.props.actions().focus(this.props.item._id)) {
this.nameInput.focus();
}
}
However, I am getting the error
Uncaught TypeError: Cannot read property 'currentItemID' of undefined
in the definition of the focus method, within the actions methods. Can anyone point me in the right direction as to why I'm getting the error or an alternative way to pass down multiple actions to child components?
the context is not passed to the function, then the 'this' in the function is that of the function itself and not the component.. you can solve it that way (put the functions in the components):
actions() {
return {
insertItem: this.insertItem.bind(this),
setCurrentItem: this.setCurrentItem.bind(this),
focus: this.focus.bind(this),
}
}
insertItem(itemID) {
const currentItems = this.state.items;
const itemPosition = this.state.items.map((item) => item._id).indexOf(itemID);
const blankItem = {
_id: (new Date().getTime()),
content: ''
};
currentItems.splice(itemPosition + 1, 0, blankItem)
this.setState({
items: currentItems,
lastAddedItemID: blankItem._id
});
},
setCurrentItem(itemID) {
this.setState({ currentItemID: itemID });
},
focus(itemID) {
return (itemID === this.state.currentItemID);
}
but yet, the recomended way is to put the functions in the components like above and remove the actions method and do this:
<Item key={item._id} item={item} actions={{
insertItem: this.insertItem.bind(this),
setCurrentItem: this.setCurrentItem.bind(this),
focus: this.focus.bind(this)
}} />
or
<Item key={item._id} item={item} actions={{
insertItem: () => this.insertItem(),
setCurrentItem: () => this.setCurrentItem(),
focus: () => this.focus()
}} />
Related
I'm using react 16.13.1 and react-dom 16.13.1. I create a ref using React.createRef() and attach to a component I defined by myself.And then I want to use a method that I defined in that component, but it does not work because .current is null.Here's my code.
class SomeComponent {
//ref
picturesRef = React.createRef();
richTextRef = React.createRef();
componentDidMount() {
console.log("this.picturesRef", this.picturesRef);
this.setState({ operation: "update" });
const product = this.props.products.find(
(item) => item._id === this.props.match.params.id,
);
const {
name,
price,
categoryId,
imgs,
desc,
detail,
} = product;
this.setState({
name,
price,
categoryId,
imgs,
desc,
detail,
});
this.picturesRef.current.setFileList(imgs);
}
render() {
const {
categories,
isLoading,
name,
price,
categoryId,
desc,
detail,
} = this.state;
return (
<Card title={<div>Add Product</div>} loading={isLoading}>
<Form
{...layout}
onFinish={this.onFinish}
onFinishFailed={this.onFinishFailed}
initialValues={{
name,
price,
categoryId,
desc,
detail,
}}
>
<Item label="Product Pictures" name="imgs">
{/**Here I attach picturesRef to this component */}
<PicturesWall ref={this.picturesRef} />
</Item>
<Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Item>
</Form>
</Card>
);
}
}
(P.S. When I use this.picturesRef.current in onFinish(), it works fine.)
Below is the code in PicturesWall
import React, { Component } from "react";
import { Upload, Modal, message } from "antd";
import { PlusOutlined } from "#ant-design/icons";
import { BASE_URL } from "../../config";
import { reqPictureDelete } from "../../api";
function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
}
class PicturesWall extends Component {
state = {
previewVisible: false,
previewImage: "",
previewTitle: "",
fileList: [],
};
handleCancel = () => this.setState({ previewVisible: false });
handlePreview = async (file) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
this.setState({
previewImage: file.url || file.preview,
previewVisible: true,
previewTitle:
file.name ||
file.url.substring(file.url.lastIndexOf("/") + 1),
});
};
handleChange = ({ file, fileList }) => {
console.log("file=", file);
const { response, status } = file;
if (status === "done") {
if (response.status === 0) {
fileList[fileList.length - 1].url = response.data.url;
fileList[fileList.length - 1].name = response.data.name;
} else {
message.error(response.msg, 1);
}
}
if (status === "removed") {
this.deleteImg(file.name);
}
this.setState({ fileList });
};
deleteImg = async (name) => {
const response = await reqPictureDelete(name);
if (response.status === 0) {
message.success("Successfully Delete", 1);
} else {
message.error("Failed", 1);
}
};
getImgNames() {
let imgs = [];
this.state.fileList.forEach((item) => {
imgs.push(item.name);
});
return imgs;
}
setFileList = (imgNames) => {
let fileList = [];
imgNames.forEach((item, index) => {
fileList.push({
uid: index,
name: item,
url: `${BASE_URL}/upload/${item}`,
});
});
this.setState(fileList);
};
render() {
const {
previewVisible,
previewImage,
fileList,
previewTitle,
} = this.state;
const uploadButton = (
<div>
<PlusOutlined />
<div className="ant-upload-text">Upload</div>
</div>
);
return (
<div className="clearfix">
<Upload
action={`${BASE_URL}/manage/img/upload`}
method="post"
listType="picture-card"
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleChange}
name="image"
>
{fileList.length >= 4 ? null : uploadButton}
</Upload>
<Modal
visible={previewVisible}
title={previewTitle}
footer={null}
onCancel={this.handleCancel}
>
<img
alt="example"
style={{ width: "100%" }}
src={previewImage}
/>
</Modal>
</div>
);
}
}
export default PicturesWall;
In the first line of componentDidMount, I print out this.picturesRef, and something weird happens:
in the first line, it shows that current is null, but when I open it, it seems that it has content. However, when I print .current, it is still null.
As I indicated in the comments section of the OP's question, I noticed that the Card component has a prop loading
<Card title={<div>Add Product</div>} loading={isLoading}>
<Form>
<Item>
<PicturesWall ref={this.picturesRef} />
...
This led me to believe that the Card component has conditions which prevented its children from rendering until it is finished loading, an example of this is instead of rendering its children while it's loading - it renders a "is-loading" type of component.
In this scenario, this.picturesRef.current will will return null on the componentDidMount lifecycle because the ref will not be referring to anything because it is not yet in the DOM by that time.
My original comment:
This post might shed some light. You have props such as loading on
Card which makes me think that perhaps you are initially rendering
some "is-loading" type of component on the DOM rather than the
children of Card such as the PicturesWall component. This could be why
PicturesWall ref is not accessible on the componentDidMount lifecycle
This doesn't directly answer your question, but I think you may be Reacting it wrong.
Your componentDidMount function seems to be basically only deriving state from props (and then calling a function on the reffed component). You can derive the state in a class component's constructor, e.g. something like
constructor(props) {
const product = props.products.find((item)=>item._id === props.match.params.id);
const {name,price,categoryId,imgs,desc,detail} = product;
this.state = {name,price,categoryId,imgs,desc,detail};
}
Then, instead of having a setFileList function, you would similarly pass the fileList down to PictureWall as a prop, e.g.
<PicturesWall fileList={this.state.imgs} />
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 trying to use this onChange function from a third party library for table pagination, it provides the pagenum when you click on a page number, and I can then provide that to my table, from a separate library. But when I call the changePage function to update the table with new pagenumber it doesn't update. I tried to bind the function once and it did update, then it threw an error 'bind of undefined' etc. How do I get it to update?
class DataTable extends React.Component {
constructor(props) {
super(props);
this.state = {
opent: false,
openid: null,
activePage: 1,
};
this.ttipOpenHandler = this.ttipOpenHandler.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
this.options = {
alwaysShowAllBtns: props.alwaysShowAllBtns,
defaultSortName: props.defaultSortName,
defaultSortOrder: props.defaultSortOrder,
firstPage: props.firstPage,
hideSizePerPage: props.hideSizePerPage,
lastPage: props.lastPage,
nextPage: props.nextPage,
noDataText: props.noDataText,
paginationShowsTotal: props.paginationShowsTotal,
paginationSize: props.paginationSize,
paginationPanel: (pgprops) => {
console.log();
return (
<div>
<Pagination
activePage={this.state.activePage}
itemsCountPerPage={20}
totalItemsCount={props.data.length}
pageRangeDisplayed={5}
onChange={(pagenum) => {
pgprops.changePage(pagenum);// THIS
this.setState({ activePage: pagenum });
}}
/>
</div>
);
},
prePage: props.prePage,
sizePerPage: props.sizePerPage,
withFirstAndLast: props.withFirstAndLast,
};
}
// only show tooltip for truncated text.
ttipOpenHandler = (event) => {
let openttip = null;
const e = event.target;
const diff = e.scrollHeight - e.offsetHeight;
if (diff > 1) {
openttip = true;
} else {
openttip = false;
}
const eid = event.target.dataset.openid;
this.setState({ opent: openttip, openid: eid });
}
handlePageChange = (pagenum, changepage) => {
changepage(this.state.activePage);
this.setState({ activePage: pagenum });
};
render() {
console.log(this.state.activePage);
return (
<div className={CLASS_NAME}>
<BootstrapTable
bodyContainerClass={this.props.bodyContainerClass}
bordered={this.props.bordered}
condensed={this.props.condensed}
containerClass={this.props.containerClass}
containerStyle={this.props.containerStyle}
data={this.props.data}
expandComponent={this.props.expandComponent}
expandableRow={this.props.expandableRow}
headerContainerClass={this.props.headerContainerClass}
height={this.props.height}
hover={this.props.hover}
keyField={this.props.keyField}
options={this.options}
pagination={this.props.pagination}
tableBodyClass={this.props.tableBodyClass}
tableContainerClass={this.props.tableContainerClass}
tableHeaderClass={`datatable-custom-header ${this.props.tableHeaderClass}`}
tableStyle={this.props.tableStyle}
trClassName={this.props.trClassName}
width={this.props.width}
>
{_.map(this.props.columns, o => (
<TableHeaderColumn
caretRender={o.caretRender}
className={o.colHeaderCss}
columnClassName={`${o.colCss} custom-td`}
headerAlign={o.headerAlign}
dataAlign={o.dataAlign}
dataField={o.key}
dataFormat={(cell, row, smth, index) => (<div className="cell-container">{o.formatter ? o.formatter(cell, row, smth, index, ReadTooltip, this.ttipOpenHandler) : <ReadTooltip data-openid={`${this.props.columns.indexOf(o)}${index}`} title={cell} open={this.state.opent && `${this.props.columns.indexOf(o)}${index}` === `${this.state.openid}`} onOpen={this.ttipOpenHandler} placement={this.props.ttipplacement}><div className="line-clamp-2">{cell}</div></ReadTooltip>}</div>)}
dataSort={o.dataSort}
isKey={o.isPrimaryKey}
key={o.key}
sortFunc={o.sortFunc}
width={o.width}
>
{o.text}
</TableHeaderColumn>
))}
</BootstrapTable>
</div>
);
}
}
this can only be accessed in binded functions part of the DataTable component used by the Pagination component. Thus, to set the active page in DataTable, the handler should be binded.
pgprops is passed from the BootstrapTable component.
Merging these will have:
constructor(props) {
super(props);
this.handlePageChange = this.handlePageChange.bind(this);
}
setActivePage(pagenum) {
this.setState({
activePage: pagenum,
});
}
handlePageChange = (pgprops, pagenum) => {
pgprops.changePage(pagenum);
this.setActivePage(pagenum);
}
// options
paginationPanel: (pgprops) => {
onChange = (pagenum) => (pgprops, pagenum) => this.handlePageChange(pgprops, pagenum),
}
Expected effect: click button -> call function setEditing() -> call function item() inside setEditing() -> this.state.isEditing changes to true -> in parent this.state.isEdit changes to true. When I call the item () function, the value of isEditing does not change
App
class App extends React.Component {
constructor() {
super();
this.state = {
isEdit = false;
};
}
handleSomething = (value) => {
this.setState(prevState => {
return {
isEdit: value
};
});
}
render() {
return (
<div>
<ul>
{
this.state.todos
.map((todo, index) =>
<Todo
key={index}
index={index}
todo={todo}
handleSomething={this.handleSomething}
/>
)
}
</ul>
</div>
);
}
}
Todo
class Todo extends Component {
state = {
isEditing: false
}
setEditing = () => {
this.setState({
isEditing: !this.state.isEditing
})
this.item();
}
item = () => {
const { isEditing} = this.state;
this.props.handleSomething(isEditing);
}
render() {
return (
<button onClick={() => this.setEditing()}>Edit</button>
)
}
}
You'll need to call this.item after the state was changed, something like
setEditing = () => {
this.setState({
isEditing: !this.state.isEditing
}, this.item)
}
Also, if you want to derive a new state form the old one, you'll have to use something like this:
setEditing = () => {
this.setState(prevState => ({
isEditing: !prevState.isEditing
}), this.item)
}
Try basing your state change on the previous state, and call parent function in a callback :
setEditing = () => {
this.setState(prevState => ({
isEditing: !prevState.isEditing
}), this.item)
}
Because as written in the React doc :
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
(https://reactjs.org/docs/react-component.html#setstate)
class Todo extends React.Component {
state = {
isEditing: false
}
setEditing = () => {
this.setState({
isEditing: !this.state.isEditing
},this.item())
}
item = () => {
const { isEditing} = this.state;
this.props.handleSomething(isEditing);
}
render() {
return (
<button onClick={() => this.setEditing()}>
Edit
</button>
)
}
}
class App extends React.Component {
constructor() {
super();
this.state = {
isEdit : false,
todos : [
"test 1",
"test 2"
]
};
}
handleSomething = (value) => {
this.setState(prevState => {
return {
isEdit: value
};
});
}
render() {
return (
<div>
<ul>
{
this.state.todos
.map((todo, index) =>
<Todo
key={index}
index={index}
todo={todo}
handleSomething={this.handleSomething}
/>
)
}
</ul>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I have a simple search bar which uses a react-autosuggest. When I create a suggestion, I want to attach an onClick handler. This onClick has been passed down from a parent class. When the suggestion is rendered however, this is undefined and therefore the click handler is not attached.
I have attached the component below, the logic which is not working is in the renderSuggestion method.
import Autosuggest from 'react-autosuggest'
import React from 'react'
export class SearchBar extends React.Component {
static getSuggestionValue(suggestion) {
return suggestion;
}
static escapeRegexCharacters(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
constructor(props) {
super(props);
this.state = {
value: '',
suggestions: [],
listOfValues: this.props.tickers
};
}
onChange = (event, { newValue, method }) => {
this.setState({
value: newValue
});
};
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: this.getSuggestions(value)
});
};
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
renderSuggestion(suggestion) {
return (
<span onClick={() => this.props.clickHandler(suggestion)}>{suggestion}</span>
);
}
getSuggestions(value) {
const escapedValue = SearchBar.escapeRegexCharacters(value.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp('^' + escapedValue, 'i');
return this.state.listOfValues.filter(ticker => regex.test(ticker));
}
render() {
const { value, suggestions } = this.state;
const inputProps = {
placeholder: "Search for stocks...",
value,
onChange: this.onChange
};
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={SearchBar.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
inputProps={inputProps} />
);
}
}
This is becuase you need to bind "this" to your function.
If you add this code to your constructor
constructor(props) {
super(props);
this.state = {
value: '',
suggestions: [],
listOfValues: this.props.tickers
};
//this line of code binds this to your function so you can use it
this.renderSuggestion = this.renderSuggestion.bind(this);
}
It should work. More info can be found at https://reactjs.org/docs/handling-events.html
In the scope of renderSuggestion, this isn't referring to the instance of the class.
Turning renderSuggestion into an arrow function like you've done elsewhere will ensure that this refers to the instance of the class.
renderSuggestion = (suggestion) => {
return (
<span onClick={() => this.props.clickHandler(suggestion)}>{suggestion}</span>
);
}