I have a following example simple page:
App.js:
export default class App extends React.Component {
render() {
return <Router>
<Switch>
<Route exact path='/' component={ArticlesPage}/>
<Route path='/search' component={SearchPage}/>
</Switch>
</Router>
};
};
ArticlesPage.js:
export default class ArticlesPage extends React.Component {
constructor(props) {
super(props);
}
render() {
return <Grid>
<Row>
<Col lg={12}>
<SearchBox/>
</Col>
</Row>
<Row>
<Col lg={12}>
articles
</Col>
</Row>
</Grid>;
}
};
SearchPage.js:
export default class SearchPage extends React.Component {
constructor(props) {
super(props);
const {q} = queryString.parse(location.search);
this.state = {
query: q
};
}
render() {
return <Grid>
<Row>
<Col lg={12}>
<SearchBox/>
</Col>
</Row>
<Row>
<Col lg={12}>
search {this.state.query}
</Col>
</Row>
</Grid>;
}
};
SearchBox.js:
export default class SearchBox extends React.Component {
constructor(props) {
super(props);
this.state = {
q: ''
};
}
onFormSubmit = (e) => {
e.preventDefault();
const {router} = this.context;
router.history.push('/search?q=' + this.state.q);
};
handleChange = (e) => {
this.setState({q: e.target.value});
};
render() {
return <form onSubmit={this.onFormSubmit}>
<Col lg={10} lgOffset={1}>
<FormGroup>
<input type="text" name="q" id="q" ref={i => this.searchInput = i} onChange={this.handleChange} />
</FormGroup>
</Col>
</form>;
}
};
And now, when I'm on the index page and type something in the input next send form, React render SearchPage.js and return correctly text search *and what I typed*, try again type something else in the input and send form, and React still show my previous text (not rerender).
What can be wrong with this simple page?
You have two different state variables, query on <SearchPage /> and q on <SearchBox />. What you are changing is q, but the variable you are rendering as text is query.
You need to lift state up and pass query as prop to <SearchPage />.
Here's why the text on SearchPage doesn't update: the constructor runs once and updates the variable in state, but when the app re-renders, React, wanting to be efficient, sees that it would re-render a new SearchPage in the same spot as the previous one, so instead of replacing it, it keeps the state of the old one. Because of this, SearchPage's state still keeps the old q variable.
Here's how you can fix it: make your SearchPage accept the search query as a prop, and render that.
class SearchPage extends React.Component {
render() {
return (
<Grid>
<Row>
<Col lg={12}>
<SearchBox />
</Col>
</Row>
<Row>
<Col lg={12}>search {this.props.query}</Col>
</Row>
</Grid>
)
}
}
In the parent, where the route for it is being rendered, use a render function, take the props of it, parse the actual query from props.location.search, and pass it directly to SearchPage.
<Route
path="/search"
render={props => <SearchPage query={getSearchQuery(props.location.search)} />}
/>
// utility function to keep things clean
function getSearchQuery(locationSearch) {
return queryString.parse(locationSearch.slice(1)).q
}
Here's a working demo.
Related
react-router-dom v5 and React 16
My loading app component contains:
ReactDOM.render(
<FirebaseContext.Provider value={new Firebase()}>
<BrowserRouter>
<StartApp />
</BrowserRouter>,
</FirebaseContext.Provider>,
document.getElementById("root")
);
I have a route component which contains:
{
path: "/member/:memberId",
component: MemberForm,
layout: "/admin"
},
Admin component:
return (
<>
<div className="main-content" ref="mainContent">
<LoadingComponent loading={this.props.authState.loading}>
<AdminNavbar
{...this.props}
brandText={this.getBrandText(this.props.location.pathname)}
/>
<AuthDetailsProvider>
<Switch>{this.getRoutes(routes)}</Switch>
</AuthDetailsProvider>
<Container fluid>
<AdminFooter />
</Container>
</LoadingComponent>
</div>
</>
)
this.getRoutes in the Switch contains the reference route above.
Now from one of my component pages I can navigate to /member/{memberid} this works fine.
the route loads a component called MemberForm
inside MemberForm I have a row that contains this method:
<Row>
{ this.displayHouseholdMembers() }
</Row>
displayHouseholdMembers = () => {
const householdDetails = this.state.family;
if (householdDetails) {
return householdDetails.map((key, ind) => {
if (key['uid'] != this.state.memberKeyID) {
return (
<Row key={ind} style={{ paddingLeft: '25px', width: '50%'}}>
<Col xs="5">
<Link to={ key['uid'] }>
{ key['first'] + " " + key['last'] }
</Link>
</Col>
<Col xs="4">
{ key['relation'] }
</Col>
<Col xs="3">
<Button
color="primary"
size="sm"
onClick={(e) => this.removeHouseRelation(key)}
>
Remove
</Button>
</Col>
</Row>
);
}
});
}
};
MemberForm:
in componentDidMount I do an firebase call to check for the data pertaining to the user using the uid aka memberId in the URL.
class MemberForm extends React.Component {
constructor(props) {
super(props);
this.state = {
...INITIAL_STATE,
currentOrganization: this.props.orgID,
householdRelation: ['Spouse', 'Child', 'Parent', 'Sibling'],
householdSelected: false,
};
}
componentDidMount() {
let urlPath, personId;
urlPath = "members";
personId = this.props.match.params.memberId;
// if it is a member set to active
this.setState({ statusSelected: "Active" })
this.setState({ memberSaved: true, indiUid: personId });
// this sets visitor date for db
const setVisitorDate = this.readableHumanDate(new Date());
this.setState({ formType: urlPath, visitorDate: setVisitorDate }, () => {
if (personId) {
this.setState({ memberSaved: true, indiUid: personId });
this.getIndividualMemberInDB(
this.state.currentOrganization,
personId,
this.state.formType,
INITIAL_STATE
);
}
});
}
...
return (
<>
<UserHeader first={s.first} last={s.last} />
{/* Page content */}
<Container className="mt--7" fluid>
<Row>
...
<Row>
{ this.displayHouseholdMembers() }
</Row>
</Form>
</CardBody>
) : null}
</Card>
</Col>
</Row>
<Row>
<Col lg="12" style={{ padding: "20px" }}>
<Button
color="primary"
onClick={e => this.submitMember(e)}
size="md"
>
Save Profile
</Button>
{ this.state.indiUid ? (
<Button
color="secondary"
onClick={e => this.disableProfile()}
size="md"
>
Disable Profile
</Button>
) : null }
</Col>
</Row>
</Container>
</>
);
When I click on the Link it shows the url has changed 'members/{new UID appears here}' but the page does not reload. I believe what's going on is that since it's using the same route in essence: path: "/member/:memberId"it doesn't reload the page. How can I get it to go to the same route but with the different memberId?
You are correct that the MemberForm component remains mounted by the router/route when only the path param is updating. Because of this the MailForm component needs to handle prop values changing and re-run any logic depending on the prop value. The componentDidUpdate is the lifecycle method to be used for this.
Abstract the logic into a utility function that can be called from both componentDidMount and componentDidUpdate.
Example:
getData = () => {
const urlPath = "members";
const { memberId } = this.props.match.params;
// this sets visitor date for db
const setVisitorDate = this.readableHumanDate(new Date());
this.setState(
{
// if it is a member set to active
statusSelected: "Active",
memberSaved: true,
indiUid: memberId,
formType: urlPath,
visitorDate: setVisitorDate
},
() => {
if (memberId) {
this.setState({ memberSaved: true, indiUid: memberId });
this.getIndividualMemberInDB(
this.state.currentOrganization,
memberId,
this.state.formType,
INITIAL_STATE
);
}
}
);
}
The lifecycle methods:
componentDidMount() {
this.getData();
}
componentDidUpdate(prevProps) {
if (prevProps.match.params.memberId !== this.props.match.params.memberId) {
this.getData();
}
}
For react-router-dom v6, can you try with simple routing? Create a Test.js with
const Test = ()=> <h1>Test Page</h1>
Then, create a Home.js with
const Home = ()=> <Link to="/test">Test</Link>
Then, add them to route.
<BrowserRouter>
<Routes>
<Route path="/" element={<Home/>} />
<Route path="/test" element={<Test />} />
</Routes>
</BrowserRouter>
Does your component structure look like this? For index route, look more at https://reactrouter.com/docs/en/v6/getting-started/overview.
I have two different react components placed one after the other in my app named SearchBar and InfiniteScroller;
function App() {
const [searchTerm, setSearchTerm] = useState("");
return (
<div className="App">
<SNavbar></SNavbar>
<MainLogo></MainLogo>
<SearchBar search={setSearchTerm}></SearchBar>
<hr/>
<InfiniteScroller term={searchTerm}/>
<Footer/>
</div>
);
}
The search bar component has its own state where it updates a search term as its input is being edited and it calls the setSearch function of its parent when the button is clicked (the function is passed as a prop in the parent)
function SearchBar(props)
{
const [search,setSearch] = useState("");
return(
<Container className="Search-Bar">
<Row>
<Col>
<InputGroup >
<FormControl
placeholder="What are we making today?"
onChange={event => setSearch(event.target.value)}
/>
<Button onClick={() => props.search(search)}>
Go!
</Button>
</InputGroup>
</Col>
</Row>
</Container>)
}
The search term that is updated by the SearchBar component is passed onto the InfiniteScroller component as a property and is set as the searchTerm field in its state object.
class InfiniteScroller extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
items:[],
page:1,
hasMore:true,
searchTerm:props.term
};
}
render(){
return(
<InfiniteScroll
dataLength={this.state.items.length}
next={this.fetchData}
hasMore={this.state.hasMore}
loader={<h4>Loading...</h4>}
endMessage={
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
>
<Row>
{this.state.items.map((i, index) => (
<Col key={index} lg="2" md="4" sm="6" xs="12">
<ImageCell className="ImageCell" link = {this.state.items[index].link}> - #{index}</ImageCell>
</Col>
))}
</Row>
</InfiniteScroll>
)
}
}
However when the setSearchTerm function of App.js is triggered by pressing the button on the SearchBar component, the InfiniteScroller does not seem to get updated. As the SearchTerm field of its state still comes up as "undefined" and the component itself does not re-render to represent the change in property.
I want the InfiniteScroller to completely re-render itself and make some API calls to populate itself with content, How can I achieve this?
So far I've tried adding in HTML tags that have the SearchTerm property in them to check if react skips re-rendering components that don't "use" any properties but that has not worked.
The props' change does not make the UI re-rendering but the states' change does.
It has 2 potential ways to fix have a proper UI re-rendering.
For the first one, you can add key attribute to your component that will help you do a trick for re-rendering whenever key gets changed
<InfiniteScroller term={searchTerm} key={searchTerm}/>
The second way, you can update your local states of that component by componentDidUpdate (useEffect in function-based components)
class InfiniteScroller extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
items:[],
page:1,
hasMore:true,
searchTerm:props.term
};
}
//update states according to props change
componentDidUpdate(prevProps) {
if(this.props.searchTerm !== prevProps.searchTerm) {
setState({ searchTerm: this.props.searchTerm })
}
}
render(){
return(
<InfiniteScroll
dataLength={this.state.items.length}
next={this.fetchData}
hasMore={this.state.hasMore}
loader={<h4>Loading...</h4>}
endMessage={
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
>
<Row>
{this.state.items.map((i, index) => (
<Col key={index} lg="2" md="4" sm="6" xs="12">
<ImageCell className="ImageCell" link = {this.state.items[index].link}> - #{index}</ImageCell>
</Col>
))}
</Row>
</InfiniteScroll>
)
}
}
Whenever I press a key and the handleKeyboardInteraction function below runs, I experience a
TypeError: this.handleKeyboardPress is not a function
What is the source of this? I'm not sure if using handleKeyPress as both a prop for a more deeply nested Component and as a regular function within my class is the issue. I tried introducing a brand new function that did the same functionality as handleKeyPress, but the error remained.
import React from "react";
import { DrumKit } from "./DrumKit.js";
import { Display } from "./Display.js";
import { Container, Row, Col } from "react-bootstrap";
import { convertCodeToKey, validKeyCodes } from "./CodeToKeyConverter.js";
export class DrumMachine extends React.Component {
constructor(props) {
super(props);
this.state = {
lastKeyPressed: ""
};
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleKeyboardInteraction = this.handleKeyboardInteraction.bind(this);
}
handleKeyPress(keyPressed) {
this.setState({
lastKeyPressed: keyPressed
});
}
handleKeyboardInteraction(event) {
const keyCode = event.keyCode;
if (validKeyCodes.includes(keyCode)) {
let keyCurrent = convertCodeToKey(keyCode);
this.handleKeyPress(keyCurrent);
}
}
componentDidMount() {
document.addEventListener("keydown", this.handleKeyboardInteraction);
}
render() {
const lastKeyPressed = this.state.lastKeyPressed;
return (
<Container>
<Row xs={2} sm={2} md={2} lg={2}>
<Col xs={10} sm={10} md={10} lg={10}>
<DrumKit
lastKeyPressed={lastKeyPressed}
onKeyPress={this.handleKeyPress}
/>
</Col>
<Col xs={2} sm={2} md={2} lg={2}>
<Display lastKeyPressed={lastKeyPressed} />
</Col>
</Row>
</Container>
);
}
}
I am trying to show my results from a JSON file only when the search button is clicked. What is the correct way to do it?
Right now as the user types a product the results are show. I have a simple filter, that is filtering the results, but I would like to make that only appear when the button is clicked. I only want to show results when the search button is clicked.
class App extends Component {
constructor(props){
super(props);
this.state = {
value: '',
list: []
}
this.handleChange = this.handleChange.bind(this);
this.handleSearch = this.handleSearch.bind(this);
this.refresh();
}
handleChange(event){
this.setState({ ...this.state, value: event.target.value })
}
refresh(){
axios.get(`${URL}`)
.then(resp => this.setState({...this.state, value: '', list: resp.data}));
}
handleSearch(product){
this.refresh();
}
render(){
return(
<div className="outer-wrapper">
<Header />
<main>
<Container>
<Row>
<Col xs={12} md={12} lg={12} className="pl-0 pr-0">
<SearchBar
handleChange={this.handleChange}
handleToggle={this.handleToggle}
handleSearch={this.handleSearch}
value={this.state.value}
/>
<SearchResultBar
value={this.state.value}
/>
<Filter />
</Col>
</Row>
<ProductList
value={this.state.value}
list={this.state.list}
/>
</Container>
</main>
</div>
)
}
}
export default App;
class Search extends Component {
constructor(props){
super(props);
}
render(){
return(
<div className="search-input">
<InputGroup>
<Input placeholder='Enter Search'
onChange={this.props.handleChange}
value={this.props.value}
/>
<InputGroupAddon className='input-group-append'
onClick={this.props.handleSearch}>
<span className='input-group-text'>
<i className="fa fa-search fa-lg fa-flip-horizontal"></i>
</span>
</InputGroupAddon>
</InputGroup>
</div>
)
}
}
export default Search;
class ProductList extends Component {
constructor(props){
super(props);
this.state = {
}
}
render(){
let filteredSearch = this.props.list.filter(
(product) => {
return product.title.indexOf(this.props.value) !== -1
}
)
return(
<Container>
<Row>
{
filteredSearch.map(item => {
return <Product {...item} key={item._id} />
})
}
</Row>
</Container>
);
}
}
export default ProductList;
As it stands, my list of products is being displayed in the app as soon as it loads. This seems something trivial, but I have been scratching my head in trying to solve it.
You're calling this.refresh() inside the constructor. So it gets run on mount.
Just remove it from the constructor and you should be fine.
I've written a container component in ReactJS and am passing in a prop to that component which is to be rendered as the 'main' content, like so:
class RegistrationContainer extends Component {
render() {
const MainContent = this.props.mainContent;
return (
<Row>
<Col offset="lg-3" lg={6}>
<MainContent />
</Col>
</Row>
);
}
}
export default RegistrationContainer;
And I'm passsing to it a mainContent prop like so:
import RegistrationContainer from './RegistrationContainer';
import RegistrationEntryView from './RegistrationEntryView';
class RegistrationCodeEntry extends Component {
render() {
return (
<RegistrationContainer mainContent={RegistrationEntryView} />
);
}
}
export default RegistrationCodeEntry;
My issue is that I would like RegistrationEntryView to have props, but can't seem to figure out how to define/pass in props on it. If I do the following I get an error:
class RegistrationCodeEntry extends Component {
render() {
const RegistrationView = <RegistrationEntryView someProp="blah" /> ;
return (
<RegistrationContainer mainContent={RegistrationView} />
);
}
}
export default RegistrationCodeEntry;
Error is as follows:
invariant.js?7313:42 Uncaught Error: Element type is invalid: expected
a string (for built-in components) or a class/function (for composite
components) but got: object. Check the render method of
RegistrationContainer.
Is this something that this.props.children could solve? I've been struggling to get my head around the concept of that, so any advice on where I'm going wrong would be appreciated.
You can solve this with this.props.children like this
class RegistrationCodeEntry extends Component {
render() {
return (
<RegistrationContainer>
// Render it as children
<RegistrationEntryView someProp="blah" />
</RegistrationContainer>
);
}
}
then in your container
class RegistrationContainer extends Component {
render() {
const MainContent = this.props.mainContent;
return (
<Row>
<Col offset="lg-3" lg={6}>
// Render the passed children
{this.props.children}
</Col>
</Row>
);
}
}
Your approach is correct. You just went wrong here:
<Row>
<Col offset="lg-3" lg={6}>
<MainContent />
</Col>
</Row>
Instead do this:
<Row>
<Col offset="lg-3" lg={6}>
{ MainContent }
</Col>
</Row>
I personally think, this approach is better than using children.
When you did this - const RegistrationView = <RegistrationEntryView someProp="blah" /> ; The component was already rendered and converted to appropriate format. Hence you cannot re-render it with <MainContent />.
So using {} is correct in this case.
Good Luck!