I have a gallery that show images, and i have a search textbox
Im Trying to use Timeout on Input event to prevent the api call on every letter im typing :
I try to handle the event with doSearch function onChange: but now I cant write anything on the textbox and it cause many errors
Attached to this session the app and gallery components
Thanks in advance
class App extends React.Component {
static propTypes = {
};
constructor() {
super();
this.timeout = 0;
this.state = {
tag: 'art'
};
}
doSearch(event){
var searchText = event.target.value; // this is the search text
if(this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(function(){this.setState({tag: event.target.value})} , 500);
}
render() {
return (
<div className="app-root">
<div className="app-header">
<h2>Gallery</h2>
<input className="input" onChange={event => this.doSearch(event)} value={this.state.tag}/>
</div>
<Gallery tag={this.state.tag}/>
</div>
);
}
}
export default App;
This is the Gallery class:
import React from 'react';
import PropTypes from 'prop-types';
import axios from 'axios';
import Image from '../Image';
import './Gallery.scss';
class Gallery extends React.Component {
static propTypes = {
tag: PropTypes.string
};
constructor(props) {
super(props);
this.state = {
images: [],
galleryWidth: this.getGalleryWidth()
};
}
getGalleryWidth(){
try {
return document.body.clientWidth;
} catch (e) {
return 1000;
}
}
getImages(tag) {
const getImagesUrl = `services/rest/?method=flickr.photos.search&api_key=522c1f9009ca3609bcbaf08545f067ad&tags=${tag}&tag_mode=any&per_page=100&format=json&safe_search=1&nojsoncallback=1`;
const baseUrl = 'https://api.flickr.com/';
axios({
url: getImagesUrl,
baseURL: baseUrl,
method: 'GET'
})
.then(res => res.data)
.then(res => {
if (
res &&
res.photos &&
res.photos.photo &&
res.photos.photo.length > 0
) {
this.setState({images: res.photos.photo});
}
});
}
componentDidMount() {
this.getImages(this.props.tag);
this.setState({
galleryWidth: document.body.clientWidth
});
}
componentWillReceiveProps(props) {
this.getImages(props.tag);
}
render() {
return (
<div className="gallery-root">
{this.state.images.map((dto , i) => {
return <Image key={'image-' + dto.id+ i.toString()} dto={dto} galleryWidth={this.state.galleryWidth}/>;
})}
</div>
);
}
}
First of all why do you need to use setTimeout to set value that is entered by user. I don't see any use using setTimeout in doSearch function.
The reason your doSearch function won't work because you are not binding it.
You can directly set value to tag using setState in doSearch function in following ways.
ES5 way
constructor(props){
super(props);
this.doSearch = this.doSearch.bind(this);
}
doSearch(event){
this.setState({
tag: event.target.value
});
}
ES6 way
doSearch = (event) => {
this.setState({
tag: event.target.value
});
}
Doing setState inside setTimeout in doSearch function won't work because
input tag has value assigned.
ES5 way
constructor(props){
super(props);
this.doSearch = this.doSearch.bind(this);
}
doSearch(event){
if(this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(function(){
this.setState({
tag: event.target.value
})
}.bind(this),500);
}
setTimeout in ES6 way
doSearch = (event) => {
if(this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.setState({
tag: event.target.value
})
},500);
}
Gallery component:
Check current props changes with previous change in componentWillRecieveProps to avoid extra renderings.
Try with below updated code
import React from 'react';
import PropTypes from 'prop-types';
import axios from 'axios';
import Image from '../Image';
import './Gallery.scss';
class Gallery extends React.Component {
static propTypes = {
tag: PropTypes.string
};
constructor(props) {
super(props);
this.state = {
images: [],
galleryWidth: this.getGalleryWidth()
};
}
getGalleryWidth(){
try {
return document.body.clientWidth;
} catch (e) {
return 1000;
}
}
getImages(tag) {
const getImagesUrl = `services/rest/?method=flickr.photos.search&api_key=522c1f9009ca3609bcbaf08545f067ad&tags=${tag}&tag_mode=any&per_page=100&format=json&safe_search=1&nojsoncallback=1`;
const baseUrl = 'https://api.flickr.com/';
axios({
url: getImagesUrl,
baseURL: baseUrl,
method: 'GET'
})
.then(res => res.data)
.then(res => {
if (
res &&
res.photos &&
res.photos.photo &&
res.photos.photo.length > 0
) {
this.setState({images: res.photos.photo});
}
});
}
componentDidMount() {
this.getImages(this.props.tag);
this.setState({
galleryWidth: document.body.clientWidth
});
}
componentWillReceiveProps(nextProps) {
if(nextProps.tag != this.props.tag){
this.getImages(props.tag);
}
}
shouldComponentUpdate(nextProps, nextState) {
if(this.props.tag == nextProps.tag){
return false;
}else{
return true;
}
}
render() {
return (
<div className="gallery-root">
{this.state.images.map((dto , i) => {
return <Image key={'image-' + dto.id+ i.toString()} dto={dto} galleryWidth={this.state.galleryWidth}/>;
})}
</div>
);
}
}
I am keeping tag initial value to empty as you are not doing anything with value art.
Please try with below code
class App extends React.Component {
static propTypes = {
};
constructor() {
super();
this.timeout = 0;
this.state = {
tag: '',
callGallery: false
};
}
doSearch = (event) => {
this.setState({tag: event.target.value, callGallery: false});
}
handleSearch = () => {
this.setState({
callGallery: true
});
}
render() {
return (
<div className="app-root">
<div className="app-header">
<h2>Gallery</h2>
<input className="input" onChange={event => this.doSearch(event)} value={this.state.tag}/>
<input type="button" value="Search" onClick={this.handleSearch} />
</div>
{this.state.callGallery && <Gallery tag={this.state.tag}/>}
</div>
);
}
}
export default App;
This is because you haven't bound this to your method.
Add the following to your constructor:
this.doSearch = this.doSearch.bind(this);
Also, you don't need the fat arrow notation for onChange. Just do:
onChange={this.doSearch}
onChange handler is just fine but you need to bind the setTimeout to render context.Currently,it is referring to window context.And the code as follows
doSearch(event){
var searchText = event.target.value; // this is the search text
if(this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(function(){
this.setState({
tag: event.target.value
})
}.bind(this),500);
}
Related
I have a simple word/definition app in React. There is an edit box that pops up to change definition when a user clicks on "edit". The new definition provided is updated in the state when I call getGlossary(), I see the new definition in inspector and a console.log statement in my App render() function triggers too. Unfortunately, I still have to refresh the page in order for the new definition to be seen on screen. I would think that calling set state for this.state.glossary in the App would trigger a re-render down to GlossaryList and then to GlossaryItem to update it's definition but I'm not seeing it :(.
App.js
class App extends React.Component {
constructor() {
super();
this.state = {
glossary: [],
searchTerm: '',
}
this.getGlossary = this.getGlossary.bind(this); //not really necessary?
this.handleSearchChange = this.handleSearchChange.bind(this);
this.handleAddGlossaryItem = this.handleAddGlossaryItem.bind(this);
this.handleDeleteGlossaryItem = this.handleDeleteGlossaryItem.bind(this);
//this.handleUpdateGlossaryDefinition = this.handleUpdateGlossaryDefinition.bind(this);
}
getGlossary = () => {
console.log('getGlossary fired');
axios.get('/words').then((response) => {
const glossary = response.data;
console.log('1: ' + JSON.stringify(this.state.glossary));
this.setState({ glossary }, () => {
console.log('2: ' + JSON.stringify(this.state.glossary));
});
})
}
componentDidMount = () => {
//console.log('mounted')
this.getGlossary();
}
handleSearchChange = (searchTerm) => {
this.setState({ searchTerm });
}
handleAddGlossaryItem = (glossaryItemToAdd) => {
//console.log(glossaryItemToAdd);
axios.post('/words', glossaryItemToAdd).then(() => {
this.getGlossary();
});
}
handleDeleteGlossaryItem = (glossaryItemId) => {
console.log('id to delete: ' + glossaryItemId);
axios.delete('/words', {
data: { glossaryItemId },
}).then(() => {
this.getGlossary();
});
}
render() {
console.log('render app fired');
const filteredGlossary = this.state.glossary.filter((glossaryItem) => {
return glossaryItem.word.toLowerCase().includes(this.state.searchTerm.toLowerCase());
});
return (
<div>
<div className="main-grid-layout">
<div className="form-left">
<SearchBox handleSearchChange={this.handleSearchChange} />
<AddWord handleAddGlossaryItem={this.handleAddGlossaryItem} />
</div>
<GlossaryList
glossary={filteredGlossary}
handleDeleteGlossaryItem={this.handleDeleteGlossaryItem}
getGlossary={this.getGlossary}
//handleUpdateGlossaryDefinition={this.handleUpdateGlossaryDefinition}
/>
</div>
</div>
);
}
}
export default App;
GlossaryItem.jsx
import React from 'react';
import EditWord from './EditWord.jsx';
const axios = require('axios');
class GlossaryItem extends React.Component {
constructor(props) {
super(props);
this.state = {
isInEditMode: false,
}
this.glossaryItem = this.props.glossaryItem;
this.handleDeleteGlossaryItem = this.props.handleDeleteGlossaryItem;
this.handleUpdateGlossaryDefinition = this.handleUpdateGlossaryDefinition.bind(this);
this.handleEditClick = this.handleEditClick.bind(this);
}
handleUpdateGlossaryDefinition = (updateObj) => {
console.log('update object: ' + JSON.stringify(updateObj));
axios.put('/words', {
data: updateObj,
}).then(() => {
this.props.getGlossary();
}).then(() => {
this.setState({ isInEditMode: !this.state.isInEditMode });
//window.location.reload();
});
}
handleEditClick = () => {
// display edit fields
this.setState({ isInEditMode: !this.state.isInEditMode });
// pass const name = new type(arguments); data up to App to handle with db
}
render() {
return (
<div className="glossary-wrapper">
<div className="glossary-item">
<p>{this.glossaryItem.word}</p>
<p>{this.glossaryItem.definition}</p>
<a onClick={this.handleEditClick}>{!this.state.isInEditMode ? 'edit' : 'cancel'}</a>
<a onClick={() => this.handleDeleteGlossaryItem(this.glossaryItem._id)}>delete</a>
</div>
{this.state.isInEditMode ?
<EditWord
id={this.glossaryItem._id}
handleUpdateGlossaryDefinition={this.handleUpdateGlossaryDefinition}
/> : null}
</div>
);
}
}
EditWord
import React from 'react';
class EditWord extends React.Component {
constructor(props) {
super(props);
this.state = {
definition: ''
};
this.handleUpdateGlossaryDefinition = this.props.handleUpdateGlossaryDefinition;
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
let definition = event.target.value;
this.setState({ definition });
}
handleSubmit(event) {
//console.log(event.target[0].value);
let definition = event.target[0].value;
let update = {
'id': this.props.id,
'definition': definition,
}
//console.log(update);
this.handleUpdateGlossaryDefinition(update);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit} className="glossary-item">
<div></div>
<input type="text" name="definition" placeholder='New definition' value={this.state.definition} onChange={this.handleChange} />
<input type="submit" name="update" value="Update" />
</form>
);
}
}
export default EditWord;
Thank you
One possible way I can see to fix this is to map the data to make the id uniquely identify each list item (even in case of update). We can to do this in getGlossary() by modifying the _id to _id + definition.
getGlossary = () => {
console.log('getGlossary fired');
axios.get('/words').then((response) => {
// Map glossary to uniquely identify each list item
const glossary = response.data.map(d => {
return {
...d,
_id: d._id + d.definition,
}
});
console.log('1: ' + JSON.stringify(this.state.glossary));
this.setState({ glossary }, () => {
console.log('2: ' + JSON.stringify(this.state.glossary));
});
})
}
In the constructor of GlossaryItem I set
this.glossaryItem = this.props.glossaryItem;
because I am lazy and didn't want to have to write the word 'props' in the component. Turns out this made react loose reference somehow.
If I just remove this line of code and change all references to this.glossaryItem.xxx to this.pros.glossaryItem.xxx then it works as I expect! On another note, the line of code can be moved into the render function (instead of the constructor) and that works too, but have to make sure I'm accessing variables properly in the other functions outside render.
I have a Header.js component that takes a user's input and updates its state. I want to transfer(prop?) this item into the parent component App.js where it will be put in as a parameter and the data will be console logged relative to the user's input. I don't know how to transfer the state and implement it into the API's parameter.
class Header extends Component {
constructor() {
super();
this.state = {
query: '',
}
}
handleSubmit = (e) => {
e.preventDefault();
// form's input value
let userSearch = this.state.query;
}
handleChange = (e) => {
this.setState({
query: e.target.value
});
}
render() {
return (
<header>
<form onSubmit={this.handleSubmit}>
<input
onChange={this.handleChange}
type="text"
placeholder="Search"
/>
<label className="sr-only" htmlFor="search">Search News</label>
</form>
</header>
)
}
}
export default Header
import Header from './Components/Header'
import axios from 'axios';
class App extends Component {
constructor() {
super();
this.state = {
articles: [],
}
}
componentDidMount() {
axios({
// I want this.state.query in header.js to be {parameter}
url: 'http://newsapi.org/v2/everything?q={parameter}&sortBy=popularity&apiKey=where-the-key-goes',
method: 'GET',
responseType: 'JSON',
}).then((response => {
let articles = response.data.articles;
this.setState({
articles,
isLoading: false,
})
console.log(articles);
}))
}
render() {
return (
<>
<Header />
</>
)
}
}
export default App
You could create a callback function in the App component and pass to Header as a prop:
class App extends Component {
...
handleSearch = (value) => {
axios({
url: `http://newsapi.org/v2/everything?q=${value}&sortBy=popularity&apiKey=where-the-key-goes`,
method: "GET",
responseType: "JSON",
}).then((response) => { ... });
};
render() {
return (
<>
<Header handleSearch={this.handleSearch} />
</>
);
}
}
Then use it in the Header's handleSubmit function:
handleSubmit = (e) => {
e.preventDefault();
// form's input value
let userSearch = this.state.query;
this.props.handleSearch(userSearch);
};
class Header extends Component<Props> { // add Props
...
handleSubmit = (e) => {
e.preventDefault();
// form's input value
let userSearch = this.state.query;
this.props.onValueSet(userSearch); // callback
}
...
}
class App extends Component {
...
// add callback
_setValueHandle = (value) => {
console.log(value);
this.setState({parameter: value}); // do something u want
};
render() {
return (
<>
{/* set callback */}
<Header onValueSet={this._setValueHandle} />
</>
)}
...
}
how about this?
You have to add props from App.js. Also you don't need to call the api on componentDidMount because you want have the query yet. Try this:
class Header extends Component {
static defaultProps = {
onUpdate: () => {},
onSubmission: () => {}
}
constructor() {
super();
this.state = {
query: '',
}
}
handleSubmit = (e) => {
e.preventDefault();
// form's input value
let userSearch = this.state.query;
this.props.onSubmission(this.state.query); //Send submission to parent
}
handleChange = (e) => {
this.setState({
query: e.target.value
}, () => {
this.props.onUpdate(this.state.query); //Send change to parent
});
}
render() {
return (
<header>
<form onSubmit={this.handleSubmit.bind(this)}>
<input
onChange={this.handleChange.bind(this)}
type="text"
placeholder="Search"
/>
<label className="sr-only" htmlFor="search">Search News</label>
</form>
</header>
)
}
}
export default Header
import Header from './Components/Header'
import axios from 'axios';
class App extends Component {
constructor() {
super();
this.state = {
articles: [],
query: ""
}
}
componentDidMount() {
}
request(query){
axios({
// I want this.state.query in header.js to be {parameter}
//NOTE: Query could also be this.state.query since we're updating it on change
url: 'http://newsapi.org/v2/everything?q={parameter}&sortBy=popularity&apiKey=where-the-key-goes',
method: 'GET',
responseType: 'JSON',
}).then((response => {
let articles = response.data.articles;
this.setState({
articles,
isLoading: false,
})
console.log(articles);
}))
}
render() {
return (
<>
<Header onUpdate={query => this.setState({query: query})} onSubmission={this.request.bind(this)} />
</>
)
}
}
export default App
I'm trying to reset editor content after some action completed using react-draft-wysiwyg editor. All contents cleared by using clearEditorContent method from draftjs-utils. But after clearing contents I can't able to type nothing in editor. Added the code below. Please help to solve this problem.
import React, { Component } from 'react';
import { EditorState, convertToRaw, ContentState } from 'draft-js';
import { clearEditorContent } from 'draftjs-utils'
import { Editor } from 'react-draft-wysiwyg';
import '../../../../node_modules/react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
export default class RTEditor extends Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
};
this.setDomEditorRef = ref => this.domEditor = ref;
}
onEditorStateChange: Function = (editorState) => {
this.setState({
editorState,
}, () => {
this.props.sendResult(draftToHtml(convertToRaw(this.state.editorState.getCurrentContent())));
});
};
componentWillReceiveProps(nextProps) {
if(nextProps.reset) {
this.reset();
}
}
componentDidMount() {
if(this.props.text) {
const html = `${this.props.text}`;
const contentBlock = htmlToDraft(html);
if (contentBlock) {
const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
const editorState = EditorState.createWithContent(contentState);
this.setState({ editorState, });
}
}
this.domEditor.focusEditor();
}
reset = () => {
let {editorState} = this.state;
editorState = clearEditorContent(editorState);
this.setState({ editorState });
};
render() {
const { editorState } = this.state;
return (
<Editor
ref={this.setDomEditorRef}
editorState={editorState}
wrapperClassName="rte-wrapper"
editorClassName="rte-editor"
onEditorStateChange={this.onEditorStateChange}
toolbarCustomButtons={[this.props.UploadHandler]}
/>
)
}
}
My parent component code is below,
import React, { Component } from 'react'
import { addThreadPost } from '../../../api/thread-api'
import { isEmpty } from '../../../api/common-api'
import RTEditor from './Loadable'
export default class ThreadActivity extends Component {
constructor(props) {
super(props)
this.state = {
clearEditor: false,
threadPost: ''
}
}
setPost = (post) => {
this.setState({ threadPost: post })
}
addThreadPost = () => {
let postText = this.state.threadPost.replace(/<[^>]+>/g, '');
if(!isEmpty(postText)) {
addThreadPost(this.props.match.params.id_thread, this.state.threadPost, this.state.postAttachments).then(response => {
this.setState({
clearEditor: true,
postAttachments: [],
})
});
}
else {
alert("Please enter some text in box.");
}
}
render() {
return [
<div className="commentbox-container">
<div className="form-group">
<div className="form-control">
<RTEditor
ref={node => { this.threadPost = node }}
text={""}
sendResult={this.setPost.bind(this)}
reset={this.state.clearEditor}
/>
<button className="btn-add-post" onClick={this.addThreadPost}>Add Post</button>
</div>
</div>
</div>
]
}
}
Your problem is probably that once you set ThreadActivity's state.clearEditor to true, you never set it back to false. So your this.reset() is getting called every time the component receives props. Which, incidentally, is going to be every time you try to type because you're invoking that this.props.sendResult.
The simplest fix is to make sure you change state.clearEditor back to false once the clearing is done.
Add to ThreadActivity.js:
constructor(props) {
...
this.completeClear = this.completeClear.bind(this);
}
...
completeClear = () => {
this.setState({clearEditor: false});
}
render() {
...
<RTEditor
...
completeClear={this.completeClear}
/>
...
}
And in RTEditor.js:
reset = () => {
let {editorState} = this.state;
editorState = clearEditorContent(editorState);
this.setState({ editorState });
this.props.completeClear();
};
I'm working on jest unit testing using react-test-renderer.The test cases fails and showing this error
"TypeError: this.props.myMaterials.fetch is not a function"
where this.props.notes.fetch is inside the componentWillMount.Is there any solution to fix this without using enzyme?
myComponent.jsx :
class myComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
column: this.getColumns(),
pageNotFound: false
};
}
componentWillMount() {
this.props.notes.fetch(this.props.courseId);
window.addEventListener('resize', this.handleResizeEvent);
this.handleError = EventBus.on(constants.NOTES_NOT_FOUND, () => {
this.setState({ pageNotFound: true });
});
}
componentDidMount() {
window.addEventListener('resize', this.handleResizeEvent);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResizeEvent);
this.handleError();
}
handleResizeEvent = () => {
this.setState({ column: this.getColumns() });
};
getColumns = () => (window.innerWidth > (constants.NOTES_MAX_COLUMNS * constants.NOTES_WIDTH) ?
constants.NOTES_MAX_COLUMNS :
Math.floor(window.innerWidth / constants.NOTES_WIDTH))
callback = (msg, data) => {
}
render() {
const { notes, language } = this.props;
if (this.state.pageNotFound) {
return (<div className="emptyMessage"><span>Empty</span></div>);
}
if (notes.loading) {
return (<Progress/>);
}
// To Refresh Child component will receive props
const lists = [...notes.cards];
return (
<div className="notesContainer" >
<NoteBook notesList={lists} callback={this.callback} coloums={this.state.column} />
</div>
);
}
}
myComponent.propTypes = {
notes: PropTypes.object,
courseId: PropTypes.string,
language: PropTypes.shape(shapes.language)
};
export default withRouter(myComponent);
myComponent.test.jsx:
const tree = renderer.create(
<myComponent.WrappedComponent/>).toJSON();
expect(tree).toMatchSnapshot();
});
Its pretty evident from the error that while testing you are not supplying the prop notes which is being used in your componentWillMount function. Pass it when you are creating an instance for testing and it should work.
All you need to do is this
const notes = {
fetch: jest.fn()
}
const tree = renderer.create(
<myComponent.WrappedComponent notes={notes}/>).toJSON();
expect(tree).toMatchSnapshot();
});
One more thing that you should take care is that your component names must begin with Uppercase characters.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
column: this.getColumns(),
pageNotFound: false
};
}
componentWillMount() {
this.props.notes.fetch(this.props.courseId);
window.addEventListener('resize', this.handleResizeEvent);
this.handleError = EventBus.on(constants.NOTES_NOT_FOUND, () => {
this.setState({ pageNotFound: true });
});
}
componentDidMount() {
window.addEventListener('resize', this.handleResizeEvent);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResizeEvent);
this.handleError();
}
handleResizeEvent = () => {
this.setState({ column: this.getColumns() });
};
getColumns = () => (window.innerWidth > (constants.NOTES_MAX_COLUMNS * constants.NOTES_WIDTH) ?
constants.NOTES_MAX_COLUMNS :
Math.floor(window.innerWidth / constants.NOTES_WIDTH))
callback = (msg, data) => {
}
render() {
const { notes, language } = this.props;
if (this.state.pageNotFound) {
return (<div className="emptyMessage"><span>Empty</span></div>);
}
if (notes.loading) {
return (<Progress/>);
}
// To Refresh Child component will receive props
const lists = [...notes.cards];
return (
<div className="notesContainer" >
<NoteBook notesList={lists} callback={this.callback} coloums={this.state.column} />
</div>
);
}
}
MyComponent.propTypes = {
notes: PropTypes.object,
courseId: PropTypes.string,
language: PropTypes.shape(shapes.language)
};
export default withRouter(MyComponent);
Have you tried giving your component a stub notes.fetch function?
Let isFetched = false;
const fakeNotes = {
fetch: () => isFetched = true
}
That way you can test that fetch is called without making a request. I'm not sure, but the test runner is running in node, and I think you may need to require fetch in node, and so the real notes may be trying to use the browser's fetch that does not exist.
I'm not an expert, but I believe it is good practice to use a fakes for side effects/dependencies anyway, unless the test specifically is testing the side effect/dependency.
Pass notes as props to your component like <myComponent.WrappedComponent notes={<here>} /> and also put a check like this.props.notes && this.props.notes.fetch so that even if your props aren't passed you don't get an error.
I'm implementing search with pagination in React. So far I found few examples of it, but all they use code with double setState(), before and after AJAX call to backend. For example my current solution is:
import React from "react"
import PropTypes from "prop-types"
import SearchField from "components/SearchField"
import SearchResults from "components/SearchResults"
import Item from "models/Item"
class Search extends React.Component {
constructor() {
super()
this.state = {
query: "",
page: 1,
foundItems: []
}
this.handleSearch = this.handleSearch.bind(this)
this.handlePageChange = this.handlePageChange.bind(this)
}
updateSearchResults() {
const query = this.state.query
const params = {
page: this.state.page
}
Item.search(query, params).then((foundItems) => {
this.setState({ foundItems })
})
}
handleSearch(event) {
this.setState({
query: event.target.value
}, this.updateSearchResults)
}
handlePageChange(data) {
this.setState({
page: data.selected + 1
}, this.updateSearchResults)
}
render() {
return (
<div className="search">
<SearchField onSearch={this.handleSearch} />
<SearchResults
onPageChange={this.handlePageChange}
onSelect={this.props.onSelect}
items={this.state.foundItems}
/>
</div>
)
}
}
Search.propTypes = {
onSelect: PropTypes.func.isRequired
}
export default Search
I know that I can change interface of updateSearchResults to receive query and page as arguments and then I can avoid first setState to pass values there, but it doesn't look like a good solution, because when list of search parameters will grow (sorting order, page size, filters for example) then it'll get a bit clumsy. Plus I don't like idea of manual state pre-management in handleSearch and handlePageChange functions in this way. I'm looking for a better implementation.
I am not fully sure what you are asking, but you can optimise your code a bit by doing the following:
class Search extends React.Component {
constructor() {
super()
this.page = 1;
this.query = "";
this.state = {
foundItems: []
}
this.handlePageChange = this.handlePageChange.bind(this)
}
updateSearchResults(event) {
if(typeof event === "object")
this.query = event.target.value;
const params = {
page: this.page
}
Item.search(this.query, params).then((foundItems) => {
this.setState({ foundItems })
})
}
handlePageChange(data) {
this.page = data.selected + 1;
this.updateSearchResults();
}
render() {
return (
<div className="search">
<SearchField onSearch={this.updateSearchResults} />
<SearchResults
onPageChange={this.handlePageChange}
onSelect={this.props.onSelect}
items={this.state.foundItems}
/>
</div>
)
}
}