I am very new to React. I have two components: TimePickerComponent and the TimeDurationPickerComponent.
The TimePickerComponent gets passed a TimeString(string) via props(only if initial data exists) and displays it like "08:00". Code:
class TimePickerComponent extends React.Component {
_placeholder;
_defaultTimeString;
_timeString;
_errorStatus;
_classes;
constructor({ placeholder, defaultTimeString, timeString, errorStatus, classes }) {
super();
this._placeholder = placeholder;
this._defaultTimeString = defaultTimeString;
this._timeString = timeString;
this._errorStatus = errorStatus;
this._classes = classes;
}
get Placeholder() {
return this._placeholder;
}
get DefaultTimeString() {
return this._defaultTimeString ? this._defaultTimeString : CONTROLS_CONSTANTS.DEFAULT_TIME_STRING;
}
get TimeString() {
return this._timeString;
}
get ErrorStatus() {
return this._errorStatus;
}
get Classes() {
return this._classes;
}
render() {
return <FormControl>
<TextField error={this.ErrorStatus}
label={this.Placeholder}
defaultValue={this.TimeString ? this.TimeString : this.DefaultTimeString}
className={this.Classes.layout}
type="time"
InputLabelProps={{
shrink: true
}}
/>
</FormControl>
}
}
TimePickerComponent.propTypes = {
placeholder: PropTypes.string.isRequired,
defaultTimeString: PropTypes.string,
timeString: PropTypes.string,
errorStatus: PropTypes.bool
}
export default withStyles(styles)(TimePickerComponent);
The TimeDurationPickerComponent gets passed a TimeInMinutes(number) via props. But the display is the same as of the TimePickerComponent("08:00"). Code:
class TimeDurationPickerComponent extends React.Component {
_placeholder;
_timeInMinutes;
_classes;
constructor({ placeholder, timeInMinutes, classes }) {
super();
this._placeholder = placeholder;
this._timeInMinutes = timeInMinutes;
this._classes = classes;
}
get Placeholder() {
return this._placeholder;
}
get TimeInMinutes() {
return this._timeInMinutes;
}
get Classes() {
return this._classes;
}
get TimeString() {
let timeFormat = CONTROLS_CONSTANTS.TIME_FORMATS.HOURS_MINUTES_COLON_SEPARATED;
let duration = moment.duration({ minutes: this.TimeInMinutes });
//https://github.com/moment/moment/issues/463
return moment.utc(duration.asMilliseconds()).format(timeFormat);
}
render() {
return <TimePickerComponent
placeholder={this.Placeholder}
timeString={this.TimeString}
classes={this.Classes}
/>
}
}
TimeDurationPickerComponent.propTypes = {
placeholder: PropTypes.string.isRequired,
timeInMinutes: PropTypes.number
}
export default TimeDurationPickerComponent;
To avoid code redundancy I reused my TimePickerComponent in the TimeDurationPickerComponent and just convert the TimeInMinutes in a TimeString and pass it down to the TimePickerComponent via props.
My question now: Is this a good practice how I solved this or should I use a HigherOrderComponent for that? Or should I use an inheritance approach for that? Which solution would be the best and why?
Thank you in advance.
What you've done here is probably fine. It could be done with a higher order component as well but a composition based approach like what you have won't have any performance issues and to be honest it's probably more readable than using an HOC.
On another note you should be using this.props and this.state to represent your class properties. They are build into React components and are what will cause your component to automatically re-render upon change.
It also makes your code significantly more concise so for example you could reduce your second component down to something like this:
class TimeDurationPickerComponent extends React.Component {
constructor(props) {
super(props);
}
createTimeString() {
let timeFormat = CONTROLS_CONSTANTS.TIME_FORMATS.HOURS_MINUTES_COLON_SEPARATED;
let duration = moment.duration({ minutes: this.props.TimeInMinutes });
//https://github.com/moment/moment/issues/463
return moment.utc(duration.asMilliseconds()).format(timeFormat);
}
render() {
return <TimePickerComponent
placeholder={this.props.Placeholder}
timeString={this.createTimeString()}
classes={this.props.Classes}
/>
}
}
Example of a component that uses flow:
// #flow
import React from 'react';
import './css/ToggleButton.css';
type Props = {
handleClick: Function;
label: string;
};
type LocalState = {
active: bool,
};
class ToggleButton extends React.Component<Props, LocalState> {
clickHandler: () => void;
constructor(props: Props) {
super(props);
this.state = {
active: true,
};
this.clickHandler = this.clickHandler.bind(this);
}
clickHandler() {
this.setState({ active: !this.state.active });
this.props.handleClick();
}
render() {
const buttonStyle = this.state.active ? 'toggle-btn-active' : 'toggle-btn-inactive';
return (
<button
className={`toggle-btn ${buttonStyle}`}
onClick={this.clickHandler}
>{this.props.label}
</button>
);
}
}
export default ToggleButton;
Related
I'm new to React (16.4.2), and I'm trying to understand the way it works. I don't want to complicate things with redux; I just want to know about the core react library.
I have an application, and (eventually down the children chain) there is an input, which is a component, RangeInput. It's just a wrapper component for an input.
The problem is two parts
I should be able to change the value within the range (as a user)
if there is data in the local storage, it should load it the first time. This also means that the user should still be able to alter/change the input value.
Right now with this, I see to only be able to do one of the other. I know I'm not understanding something here.
What needs to happen?
Thanks,
Kelly
Here are the classes:
export class RangeInput extends React.Component {
constructor(props) {
super(props);
this.ds = new DataStore();
this.state = {
value: props.value
};
}
static getDerivedStateFromProps(props, state) {
console.log('props', props, 'state', state);
if (props.value !== state.value) {
return {value: props.value};
}
return null;
}
onChange(event) {
const target = event.target;
this.setState({
value: target.value
});
if (this.props.onChange) {
this.props.onChange({value: target.value});
}
}
onKeyUp(event) {
if (event.keyCode !== 9) {
return;
}
const target = event.target;
if (this.props.onChange) {
this.props.onChange({value: target.value});
}
}
render() {
return <div>
<input type="number" value={this.state.value}
onChange={this.onChange.bind(this)}
onKeyUp={this.onKeyUp.bind(this)}/>
</div>;
}
}
const DATA_LOAD = 'load';
export class Application extends React.Component {
constructor() {
super();
this.state = {
value: -1,
load = DATA_LOAD
};
}
componentDidMount() {
if (this.state.load === DATA_LOAD) {
this.state.load = DATA_CLEAN;
const eco = this.ds.getObject('the-app');
if (eco) {
this.setState({value: eco});
}
}
}
render(){
return <RangeInput value={this.state.value} />;
}
}
ReactDOM.render(
<Application/>,
document.getElementById('root')
);
I think this situation can be simplified quite a bit:
import React from 'react';
export const RangeInput = props => (
<input
value={props.value}
onChange={props.setValue} />
)
export class Application extends React.Component {
constructor(props) {
super(props);
this.state = { value: -1, };
}
componentDidMount() {
var val = localStorage.getItem('myVal');
if (val) this.setState({value: val})
}
setValue(e) {
this.setState({value: e.target.value})
localStorage.setItem('myVal', e.target.value);
}
render() {
return <RangeInput
value={this.state.value}
setValue={this.setValue.bind(this)} />;
}
}
Here we have two components: <RangeInput>, a stateless component, and <Application>, the brains behind the operation.
<Application> keeps track of the state, and passes a callback function to RangeInput. Then, on keydown, <RangeInput> passes the event object to that callback function. Application then uses the event object to update the state and the localStorage. On refresh, the last saved value is fetched from localStorage and present in the input (if available).
I get confused by the 'state' and 'login' members of a class definition from a running example as below:
class Login extends React.Component {
state = {
redirectToReferrer: false
};
login = () => {
fakeAuth.authenticate(() => {
this.setState({ redirectToReferrer: true });
});
};
render() {
const { from } = this.props.location.state
|| { from: { pathname: "/" } };
const { redirectToReferrer } = this.state;
if (redirectToReferrer) {
return <Redirect to={from} />;
}
return (
<div>
<p>You must log in to view the page at {from.pathname}</p>
<button onClick={this.login}>Log in</button>
</div>
);
}
}
For the 'login', I wan to get confirmation that this is a function member of the Login class, right? I can understand the motivation of using an arrow function is a matter of binding of 'this', but I did not see this syntax appeared in my ES6 book. It looks like in the top level of {}, it just defined a variable which is assigned with an arrow function.
For the 'state', this looks like a simple assignment, but I know it must be defining a member of the 'Login' since there is a 'this.state' reference. But I don't understand the syntax, my ES6 book says any instance property must be defined in constructor of the class. Is there any other special meaning of this kind of definition?
The standard way of defining initial state in React is like this:
class Login extends React.Component {
constructor(props){
super(props);
this.state = {
redirectToReferrer: false
};
}
....
}
But, there are some libraries like unstated, that allow you to define state like this:
// BookContainer.js
import { Container } from 'unstated'
class BookContainer extends Container {
state = {
books: [],
booksVisible: false
}
addBook = book => {
const books = [...this.state.books, book]
this.setState({ books })
}
toggleVisibility = () => {
this.setState({
booksVisible: !this.state.booksVisible
})
}
}
export {
BookContainer
}
EDIT: Regarding to the login method, as you already told, is about binding the this
This:
login = () => {
fakeAuth.authenticate(() => {
this.setState({ redirectToReferrer: true });
});
};
Is the same as doing this:
class Login extends React.Component {
constructor(props){
super(props);
this.state = {
redirectToReferrer: false
};
this.login = this.login.bind(this); // Bind the this
}
login(){
fakeAuth.authenticate(() => {
this.setState({ redirectToReferrer: true }); // This is not undefined
});
}
}
You can find more info in the official unstated page
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>
)
}
}
Reading around, I see that initializing state from props in the getInitialState()/constructor can be an anti-pattern.
What is the best way of initializing state from props and managing to be consistent?
As you can see below, I'm trying to initialize my "Card" component so that I may have a likeCount and isLikedByMe states initialized. I do this so that I may have a custom like counter displayed and the text of the Like button to change, by resetting the state.
At this point, I'm doing this in the constructor, but that is the wrong way to do it. How should I manage this?
import * as React from "react";
import { CardLikeButton } from "./buttons";
export enum CardType {
None = 0,
Text,
Image
}
export interface CardMedia {
text?: string;
imageUrl?: string;
}
export interface CardDetails {
isLikedByMe: boolean;
likeCount: number;
}
export interface CardParams extends React.Props<any> {
cardType: number;
cardId: string;
cardMedia: CardMedia;
cardDetails: CardDetails;
}
export class Card extends React.Component<CardParams, CardDetails> {
state: CardDetails;
constructor(props: CardParams) {
super(props);
console.log("in card constructor");
console.log("card type: " + props.cardType);
this.state = { // setting state from props in getInitialState is not good practice
isLikedByMe: props.cardDetails.isLikedByMe,
likeCount: props.cardDetails.likeCount
};
}
componentWillReceiveProps(nextProps: CardParams) {
this.setState({
isLikedByMe: nextProps.cardDetails.isLikedByMe,
likeCount: nextProps.cardDetails.likeCount
});
}
render() {
console.log("RENDERING CARD");
// console.dir(this.props.cardDetails);
// console.dir(this.props.cardMedia);
// console.dir(this.props.cardType);
if (this.props.cardType === CardType.Text) { // status card
return (
<div className="general-card">
<p>Text card.ID: {this.props.cardId}</p>
<p>{this.props.cardMedia.text}</p>
<CardLikeButton onButClick={this.likeButtonClicked} buttonText={this.state.isLikedByMe ? "Liked" : "Like"} isPressed={this.state.isLikedByMe}/>
<p>Like count: {this.state.likeCount}</p>
</div>
);
} else { //photo card
return (
<div className="general-card">
<p>Image card.ID: {this.props.cardId}</p>
<p> {this.props.cardMedia.text} </p>
<img src={this.props.cardMedia.imageUrl} />
<br/>
<CardLikeButton onButClick={this.likeButtonClicked} buttonText={this.state.isLikedByMe ? "Liked" : "Like"} isPressed={this.state.isLikedByMe}/>
<p>Like count: {this.state.likeCount}</p>
</div>
);
}
}
likeButtonClicked = () => {
console.log('in card => like button clicked!');
var _isLikedByMe = this.state.isLikedByMe;
var _likeCount = this.state.likeCount;
if (_isLikedByMe) {
_likeCount--;
} else {
_likeCount++;
}
_isLikedByMe = !_isLikedByMe;
this.setState({
isLikedByMe: _isLikedByMe,
likeCount: _likeCount
})
}
}
Here is the main list component:
/// <reference path="../../typings/index.d.ts" />
import * as React from "react";
import * as ReactDOM from "react-dom";
import {Card} from "./card";
import {CardParams, CardType, CardMedia, CardDetails} from "./card";
var card1: CardParams = {
cardType: CardType.Image,
cardId: "card1234",
cardDetails: {
isLikedByMe: false,
likeCount: 3
},
cardMedia: {
text: "some test text; badescuga",
imageUrl: "http://www9.gsp.ro/usr/thumbs/thumb_924_x_600/2016/06/19/738742-rkx1568-lucian-sinmartean.jpg"
}
};
var card2: CardParams = {
cardId: "card35335",
cardType: CardType.Text,
cardDetails: {
isLikedByMe: true,
likeCount: 1
},
cardMedia: {
text: "some test 2 text"
}
};
var cards = [card1, card2];
ReactDOM.render(
<div>
{
cards.map((item) => {
return (
<Card key={item.cardId} cardId={item.cardId} cardType={item.cardType} cardDetails={item.cardDetails} cardMedia={item.cardMedia}/>
);
})
}
</div>,
document.getElementById("mainContainer")
);
Without getting into working with Flux, or Redux, and focusing on your question.
IMHO, state and props need to be separated, where Card only gets props, and state is managed from above. Card component will get an event handler to raise once the like button has been clicked. You could either do the "like" logic inside the Card component, and just raise the event handler with the output of that logic, for example:
this.props.likeClicked(isLikedByMe, updatedLikeCount).
Or, do the whole logic in the parent component.
I would also wrap all cards in another component.
Example:
class Card extends React.Component {
constructor(props: CardParams) {
super(props);
}
render() {
return (
<div>
<button onClick={this.likeButtonClicked}>
{this.props.isLikedByMe ? 'Unlike' : 'Like'}
</button>
<p>Like count: {this.props.likeCount}</p>
</div>
)
}
likeButtonClicked = () => {
console.log('in card => like button clicked!');
var _isLikedByMe = this.props.isLikedByMe;
var _likeCount = this.props.likeCount;
if (_isLikedByMe) {
_likeCount--;
} else {
_likeCount++;
}
_isLikedByMe = !_isLikedByMe;
if (this.props.likeUpdated) {
this.props.likeUpdated({
cardId: this.props.cardId,
isLikedByMe: _isLikedByMe,
likeCount: _likeCount
})
}
}
}
class CardList extends React.Component {
constructor(props) {
super(props)
this.state = {
// Could use es6 map
cards: {123: {isLikedByMe: false, likeCount: 3},
124: {isLikedByMe: true, likeCount: 2}}
}
}
_onLikeUpdated({cardId, isLikedByMe, likeCount}) {
const cards = Object.assign({}, this.state.cards)
cards[cardId] = {isLikedByMe, likeCount}
this.setState({cards})
}
_getCards() {
return Object.keys(this.state.cards).map(cardId => {
return <Card key={cardId}
cardId={cardId}
likeUpdated={this._onLikeUpdated.bind(this)}
{...this.state.cards[cardId]} />
})
}
render() {
return <div>
{this._getCards()}
</div>
}
}
Fiddle: https://jsfiddle.net/omerts/do13ez79/
How would one implement pagination for search using React?
Here's my code for returning users.
export default class SearchPanel extends Component {
static propTypes = {
isLoading: PropTypes.bool,
users: PropTypes.array,
}
static contextTypes = {
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
}
static defaultProps = {
isLoading: false,
users: [],
}
constructor(props, context) {
super(props, context);
}
render() {
const searchResults = (this.props.isLoading)
? <h1>LOADING USERS</h1>
: this.props.users.map((user) => <SearchResultUser key={user.username} {...user} />);
return (
<div className="ibox-content">
{this.props.users}
</div>
)
}
}
Note: I've kept most of the html out of the render to keep the code looking simple for this question.
So in a nutshell, this.props.users returns an array of users, I just need to be able to paginate the result by lets say 5 per page.
Use this function:
getUsers(page, amount) {
return this.props.users.filter(function(item, i) {
return i >= amount*(page-1) && i < page*amount
});
}
E.g {() => getUsers(1, 5)} will return users between 1-5, where {() => getUsers(2,5)} will return users between 6-10.
Example: http://codepen.io/zvona/pen/GpEdqN?editors=001