How can I conditionally render search results with ReactiveBase - reactjs

I'm trying to conditionally render my Results component from within ReactiveBase, but every time I try to use a ternary operator it breaks the rendering. If I remove the ternary, results display.
I'm using the <ReactiveList> component to display the results in my Results component.
I only want results to display if a user has ACTUALLY submitted a search query. So how can I conditionally render the Results component from within ONLY after a user has submitted a query
Here is my code so far:
import React, { Component } from 'react';
import { Redirect, withRouter } from 'react-router-dom';
import { ReactiveBase, DataSearch } from '#appbaseio/reactivesearch';
import Results from '../../components/appbaseio-search/Results';
class SearchContainer extends Component {
constructor(props) {
super(props);
this.state = {
redirect: false,
loading: false,
};
}
render() {
const { pathname } = this.props;
const { value, loading } = this.state;
const { redirect } = this.state;
if (redirect ) {
return (
<Redirect
to={{
pathname: '/search',
search: `?q="${value}"`,
}}
/>
);
}
return (
<ReactiveBase
...>
<DataSearch
...
/>
{ pathname === '/results'
? <Results />
: null
}
</ReactiveBase>
);
}
}
export default withRouter(SearchContainer);

All it came down to was putting another piece of state, and updating that with componentDidMount
this.state = {
...
loading: true,
};
componentDidMount() {
const { location } = this.props;
this.setState({ loading: false });
}
and then after render()
const { loading } = this.state;
and then the conditional
{ loading === false && location.pathname === '/results'
? <Route path="/results" component={Results} />
: null }
You can also just render the component <Results /> instead of using RR4-<Route /> - I tried both - they both work just fine.

Have you considered using React-Router? Or you can use state without relying on paths.
For instance:
render() {
const { results } = this.state;
if (results || results.length === 0) {
return (<ReactiveBase>...</ReactiveBase>);
} else {
return (<ReactiveBase><Results /></ReactiveBase>);
}
}

Related

Undefined props in componentDidMount

This is starting to get really frustrating. Basically, I cannot access props in my subcomponents. if I try to render them directly using this.props- it works, but if I need to do additional processes with them, or save them into state, I get undefined props all the time. I have a parent component, which looks something like this:
import React from 'react';
import Title from './EventSubComponents/Title';
import SessionInfo from './EventSubComponents/SessionInfo';
import SessionTime from './EventSubComponents/SessionTime';
import Location from './EventSubComponents/Location';
import Subscribers from './EventSubComponents/Subscribers';
class EventNode extends React.Component {
constructor(props) {
super(props);
this.state = {
'event': [],
}
}
componentDidMount() {
this.getEvent(this.props.location.selectedEventId);
}
getEvent(eventId) {
fetch('/api/v.1.0/event/' + eventId, {mode: 'no-cors'})
.then(function(response) {
if(!response.ok) {
console.log('Failed to get single event.');
return;
}
return response.json();
})
.then((data) => {
if (!data) {
return;
}
this.setState({
'event': data
})
});
}
render() {
return(
<div className="event-wrapper">
<Title
title = { this.state.event.title }
date = { this.state.event.start }
/>
<SessionInfo
distance = { this.state.event.distance }
type = { this.state.event.type }
/>
<SessionTime
start = { this.state.event.start }
end = { this.state.event.end }
/>
<Location location = { this.state.event.start_location }/>
<Subscribers
subscribers = { this.state.event.subscribers }
eventId = { this.state.event._id }
/>
</div>
);
}
}
export default EventNode;
And my sub-component SessionTime, which looks like this:
import React from 'react';
import moment from 'moment';
class Title extends React.Component {
constructor(props) {
super(props);
this.state = {
'title': '',
'date': '',
}
}
componentDidMount() {
console.log(this.props.title);
console.log(this.props.date);
// undefined both props.
this.convertToTitleDate(this.props.date);
this.setState({
'title': this.props.title
})
}
convertToTitleDate(date) {
var newDate = moment(date).format('dddd, Do MMMM')
this.setState({
'date': newDate,
});
}
render() {
return (
<div className="event-title-wrapper">
<h1> { this.state.title } </h1>
<div className="event-title-date"> { this.state.date } </div>
</div>
);
}
}
export default Title;
Could anyone explain, why both this.props.date and this.props.title are undefined in my componentDidMount function? I have couple more components in my EventNode and I have the same problems in them as well.
Changing componentDidMount to componentWillMount does not help. I am fairly certain I have problems in my parent EventNode component, but I cannot figure out where. Inside EventNode render() all the state variables are defined.
You initialize event to an empty array and pass down this.state.event.start and this.state.event.end to SessionTime, which will both be undefined on first render since event has not been loaded yet and there are no start and end properties on the array.
You could instead e.g. set event to null initially, and return null from the render method until the event has been loaded.
Example
class EventNode extends React.Component {
state = {
event: null
};
// ...
render() {
const { event } = this.state;
if (event === null) {
return null;
}
return (
<div className="event-wrapper">
<Title title={event.title} date={event.start} />
<SessionInfo distance={event.distance} type={event.type} />
<SessionTime start={event.start} end={event.end} />
<Location location={event.start_location} />
<Subscribers
subscribers={event.subscribers}
eventId={this.state.event._id}
/>
</div>
);
}
}

React authentication HoC

I have a React-Router-Redux application that I built with an expressJS server. Part of this application is authentication using JWT. Aside from protecting Routes, I am trying to create a HoC that will protect it's wrapped component by reaching out to the server and authenticating before displaying the wrapped component. Here is the HoC I have built:
withAuth.js:
import React, { Component } from 'react';
import {connect} from 'react-redux';
import * as actions from '../../store/actions';
export default function (ComposedComponent) {
class Authenticate extends Component {
componentWillMount() {
console.log('will mount');
this.props.authenticate();
}
render() {
const { loading, loaded } = this.props;
return !loading && loaded ? <ComposedComponent {...this.props} /> : null;
}
}
const mapStateToProps = state => {
return {
loading: state.auth.loading,
loaded: state.auth.loaded
};
};
const mapDispatchToProps = dispatch => {
return {
authenticate: () => dispatch(actions.authenticate())
};
};
return connect(mapStateToProps, mapDispatchToProps)(Authenticate)
}
I am using Redux Saga aswell. The authenticate action calls a saga that sets loading to true, loaded to false and reaches out to the server. When the server sends confirmation, loaded is set to true and loading is set to false, aside from a cookie and some data being saved.
It basically works, but the problem is that when I enter a route with this HoC, the authentication process is done twice (HoC's ComponentWillMount is called twice) and I cant figure out why. It happens with a wrapped component that doesnt even reach out to the server or change props on mount/update. What am I missing here?
This is one of the wrapped components that has this problem:
class SealantCustomer extends Component {
state = {
controls: {
...someControls
}
}
shouldComponentUpdate(nextProps) {
if (JSON.stringify(this.props.sealantCustomer) === JSON.stringify(nextProps.sealantCustomer)) return false;
else return true;
}
updateInput = (event, controlName) => {
let updatedControls = inputChangedHandler(event, controlName, this.state.controls);
this.setState({controls: updatedControls});
}
searchCustomer = async (event) => {
event.preventDefault();
this.props.fetchCustomer(this.state.controls.phone.value, this.state.controls.site.value, this.state.controls.name.value);
}
render () {
let sealantCustomer;
if (this.props.loading) {
sealantCustomer = <Loader />;
}
if (!this.props.loading) {
if (!this.props.sealantCustomer) this.props.error ? sealantCustomer = <h3 style={{color: 'salmon'}}>ERROR: {this.props.error}</h3> : sealantCustomer = <h3>Please search for a sealant customer</h3>
else if (this.props.sealantCustomer.length === 0) sealantCustomer = <h3>Found no sealant customers with these details!</h3>
else {
let data = [];
this.props.sealantCustomer.forEach(person => {
...filling data here
})
const columns = [{
...table columns
}]
const keysToSkip = [keys];
sealantCustomer = <ReactTable data={data} columns={columns} defaultPageSize={3} className={['-striped', '-highlight', 'tableDefaults'].join(" ")}
SubComponent={sub component} />
}
}
return (
<div className={classes.sealantCustomerPage}>
<SearchBox controls={this.state.controls} submit={this.searchCustomer} inputUpdate={this.updateInput} name="Sealant Customers" />
<div className={classes.sealantCustomer}>
{sealantCustomer}
</div>
</div>
)
}
};
const mapStateToProps = state => {
return {
loading: state.searches.loading,
error: state.searches.error,
sealantCustomer: state.searches.sealantCustomer
};
};
const mapDispatchToProps = dispatch => {
return {
fetchCustomer: (phone, site, name) => dispatch(actions.searchSealantCustomer(phone, site, name))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(SealantCustomer);

How to show validation message on <TagsInput> react premade component on unique value

I have an input tag component from react-tagsinput as follows:
const onTagChange = (tags) => {
const noDuplicateTags = tags.filter((v, i) => tags.indexOf(v) === i);
const duplicateEntered = tags.length !== noDuplicateTags.length;
if (duplicateEntered) {
onTagChange(tags);
console.log('duplicate');
}
onTagChange(noDuplicateTags);
};
function TagContainer({
tags,
}) {
return (
<div>
<Header>Meta:</Header>
<TagsInput value={tags} onChange={onTagChange} />
</div>
);
}
TagContainer.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string),
};
TagContainer.defaultProps = {
tags: [],
};
export default TagContainer;
and the implementation on the onTagChange method which is passed as a prop to the <TagContainer> component in another component.
export class Modal extends React.Component {
...
...
onTagChange = (tags) => {
this.props.onTagChange(tags);
}
...
...
render() {
return(
<TagContainer
tags={tags}
onTagChange={this.onTagChange}
/>
);
}
}
Problem: onlyUnique prop in the <TagsInput> component is set to true to avoid duplicate entries. But I need to display an error message saying "duplicate values" as soon as user enters a duplicate value. How can this be done especially on the third party component.
I think you're going to have to handle dealing with duplicates in your component because you are getting no feedback from <TagInput /> component.
At a higher level, I would do something like this
class Example extends React.Component {
constructor() {
super();
this.state = {
showDuplicateError: false
};
}
handleTagChange(tags) {
const uniqueTags = removeDuplicates(tags);
const duplicateEntered = tags.length !== uniqueTags.length;
if (duplicateEntered) {
this.showDuplicateError();
}
// add unique tags regardless, as multiple tags could've been entered
const { onTagChange } = this.props;
onTagChange(uniqueTags);
}
showDuplicateError() {
this.setState({
showDuplicateError: true
});
}
render() {
const { showDuplicateError } = this.state;
const { tags } = this.props;
return (
<React.Fragment>
{ showDuplicateError && <div>Duplicate entered</div>}
<TagsInput value={ tags } onTagChange={ this.handleTagChange } />
</React.Fragment>
);
}
}

this.props.history.push not re-rendering react component

In my component I use this.props.history.push(pathname:.. search:..) to rerender the component and fetch new data form a third party service. When I first call the page it renders. But when I call history push inside the component the URL updates correctly BUT the component doesn't rerender. I read a lot but couldn't get it working. Any ideas?
I'm using react router v4
//index.js
<Provider store={store}>
<BrowserRouter>
<Switch>
<Route path="/login" component={Login}/>
<Route path="/" component={Main}/>
</Switch>
</BrowserRouter>
</Provider>
//Main.js
//PropsRoute is used to push props to logs component so I can use them when fetching new data
const PropsRoute = ({ component: Component, ...rest }) => {
return (
<Route {...rest} render={props => <Component {...props} />}/>
);
};
class Main extends Component {
render() {
return (
<div className="app">
<NavigationBar/>
<div className="app-body">
<SideBar/>
<Switch>
<PropsRoute path="/logs" component={Log}/> //this component is not rerendering
<Route path="/reports" component={Reports}/>
<Route path="/gen" component={Dashboard}/>
<Redirect from="/" to="/gen"/>
</Switch>
</div>
</div>
)
}
}
export default Main;
//inside 'Log' component I call
import React, {Component} from 'react';
import {getSystemLogs} from "../api";
import {Link} from 'react-router-dom';
import _ from "lodash";
import queryString from 'query-string';
let _isMounted;
class Log extends Component {
constructor(props) {
super(props);
//check if query params are defined. If not re render component with query params
let queryParams = queryString.parse(props.location.search);
if (!(queryParams.page && queryParams.type && queryParams.pageSize && queryParams.application)) {
this.props.history.push({
pathname: '/logs',
search: `?page=1&pageSize=25&type=3&application=fdce4427fc9b49e0bbde1f9dc090cfb9`
});
}
this.state = {
logs: {},
pageCount: 0,
application: [
{
name: 'internal',
id: '...'
}
],
types: [
{
name: 'Info',
id: 3
}
],
paginationPage: queryParams.page - 1,
request: {
page: queryParams.page === undefined ? 1 : queryParams.page,
type: queryParams.type === undefined ? 3 : queryParams.type,
pageSize: queryParams.pageSize === undefined ? 25 : queryParams.pageSize,
application: queryParams.application === undefined ? 'fdce4427fc9b49e0bbde1f9dc090cfb9' : queryParams.application
}
};
this.onInputChange = this.onInputChange.bind(this);
}
componentDidMount() {
_isMounted = true;
this.getLogs(this.state.request);
}
componentWillUnmount() {
_isMounted = false;
}
getLogs(request) {
getSystemLogs(request)
.then((response) => {
if (_isMounted) {
this.setState({
logs: response.data.Data,
pageCount: (response.data.TotalCount / this.state.request.pageSize)
});
}
});
}
applyFilter = () => {
//reset page to 1 when filter changes
console.log('apply filter');
this.setState({
request: {
...this.state.request,
page: 1
}
}, () => {
this.props.history.push({
pathname: '/logs',
search: `?page=${this.state.request.page}&pageSize=${this.state.request.pageSize}&type=${this.state.request.type}&application=${this.state.request.application}`
});
});
};
onInputChange = () => (event) => {
const {request} = this.state; //create copy of current object
request[event.target.name] = event.target.value; //update object
this.setState({request}); //set object to new object
};
render() {
let logs = _.map(this.state.logs, log => {
return (
<div className="bg-white rounded shadow mb-2" key={log.id}>
...
</div>
);
});
return (
<main className="main">
...
</main>
);
}
}
export default Log;
Reactjs don't re-run the constructor method when just props or state change, he call the constructor when you first call your component.
You should use componentDidUpdate and do your fetch if your nextProps.location.pathname is different than your this.props.location.pathname (react-router location)
I had this same issue with a functional component and I solved it using the hook useEffect with the props.location as a dependency.
import React, { useEffect } from 'react';
const myComponent = () => {
useEffect(() => {
// fetch your data when the props.location changes
}, [props.location]);
}
This will call useEffect every time that props.location changes so you can fetch your data. It acts like a componentDidMountand componentDidUpdate.
what about create a container component/provider with getderivedstatefromprops lifecycle method, its more react-look:
class ContainerComp extends Component {
state = { needRerender: false };
static getderivedstatefromprops(nextProps, nextState) {
let queryParams = queryString.parse(nextProps.location.search);
if (!(queryParams.page && queryParams.type && queryParams.pageSize && queryParams.application)) {
return { needRefresh: true };
} else {
return { needRefresh: false };
}
}
render() {
return (
<div>
{this.state.needRefresh ? <Redirect params={} /> : <Log />}
</div>
);
}
}

Cannot access props in Higher Order Component React

I have a simple class as follows that for the sake of this example just renders out the length of a list loaded from Firebase.
class Companies extends Component {
constructor() {
super();
this.state = {
companies: [],
loading: true
};
this.firebase = new FirebaseList('companies');
}
componentDidMount() {
const previousCompanies = this.state.companies;
this.firebase.databaseSnapshot('companies').then((snap) => {
if (snap.val() === null) {
this.setState({loading: false})
}
});
this.firebase.database.on('child_added', snap => {
previousCompanies.push({
id: snap.key,
...snap.val()
});
this.setState({
companies: previousCompanies,
loading: false
})
});
this.firebase.database.on('child_changed', snap => {
const updatedCompanies = updatedItems(this.state.companies, this.state.currentCompany);
this.setState({
companies: updatedCompanies
})
});
this.firebase.database.on('child_removed', snap => {
const updatedCompanies = removeItem(previousCompanies, snap.key);
this.setState({
companies: updatedCompanies
})
})
}
render() {
return (
<div>
{this.state.companies.length}
</div>
);
}
}
export default WithLoader('companies')(Companies);
This is a pattern I frequently repeat, so I want to build a Loader into a Higher Order Component, to show a Loader animation when the data is being fetched from the database.
I'm using the following code for this:
const WithLoader = (propName) => (WrappedComponent) => {
return class WithLoader extends Component {
componentDidMount() {
console.log(this.props)
}
isEmpty(prop) {
return (
prop === null ||
prop === undefined ||
(prop.hasOwnProperty('length') && prop.length === 0) ||
(prop.constructor === Object && Object.keys(prop).length === 0)
)
}
render() {
return this.isEmpty(this.props[propName]) ? <Spinner /> : <WrappedComponent {...this.props}/>
}
}
};
export default WithLoader;
I'm trying to access the companies from the state of the Companies component in my Higher Order Component. However, when I console.log(this.props) in my Higher Order Component, I only get the history, match and location props.
What am I doing wrong?
since withLoader is the HOC so first withLoader will be rendered before the Companies component, because of which you are getting only routes and match as props.
since withLoader is wrapping the companies component so it will access the props of its parent where it is rendered not the props of its wrapped component.
according to your query there are two ways
either you fetch all the data in HOC and then render the Companies Component
or
pass the fetched data from parent to the Companies component.
<Companies companylist={this.state.company} />
what you are trying to do is that you are trying to pass the props of its child which is not yet rendered.

Resources