I need help, I have a component, and your function is render de header of app, with right and left icon, and in center is title of current page.
But, I can re-render the title of page, but the icon not re-render.
I not have idea for the solution this.
MyCode of Header.
import React, { Component } from 'react';
import ReactNative from 'react-native';
import { Icon, Text } from './../labsoft.ui';
import styles from './styles';
const Header = ReactNative.StyleSheet.flatten(styles.header);
const BoxHeaderFlex = ReactNative.StyleSheet.flatten(styles.boxHeaderFlex);
const BoxHeaderIcon = ReactNative.StyleSheet.flatten(styles.boxHeaderIcon);
const BoxHeaderTouchable = ReactNative.StyleSheet.flatten(styles.BoxHeaderTouchable);
const BoxHeaderTouchableCenter = ReactNative.StyleSheet.flatten(styles.BoxHeaderTouchableCenter);
interface HeaderProperties {
leftAction?: HeaderLeftAction,
rightAction?: HeaderRightAction,
title?: string;
style?: Style;
}
interface HeaderState {
leftAction?: HeaderLeftAction,
rightAction?: HeaderRightAction,
title?: string;
}
interface HeaderLeftAction {
icon: string;
onClick?: () => void
}
interface HeaderRightAction {
icon: string;
onClick?: () => void
}
interface Style { }
export default class HeaderComponent extends Component<HeaderProperties, HeaderState> {
constructor(props: HeaderProperties) {
super(props);
this.state = {
leftAction: this.props.leftAction,
rightAction: this.props.rightAction,
title: this.props.title
}
}
public setLeftAction(action: HeaderLeftAction) {
this.setState({
leftAction: action
});
}
public setRightAction(action: HeaderRightAction) {
this.setState({
rightAction: action
});
}
public setTitle(title: string) {
this.setState({
title: title
});
}
render() {
console.log('props: ', this.props.rightAction.icon);
console.log('state: ', this.state.rightAction.icon);
let iconRight = this.state.rightAction.icon;
let iconLeft = this.state.leftAction.icon;
return (
<ReactNative.View style={[BoxHeaderFlex, { ...this.props.style }]}>
{
this.state.leftAction != null ?
<Icon icon={iconLeft} onPress={this.state.leftAction.onClick} />
:
<ReactNative.TouchableOpacity style={BoxHeaderTouchable}>
<ReactNative.View>
</ReactNative.View>
</ReactNative.TouchableOpacity>
}
{
this.state.title != null ?
<Text style={BoxHeaderTouchableCenter}>{this.state.title}</Text>
:
<Text style={BoxHeaderTouchableCenter} />
}
{
this.state.rightAction != null ?
<Icon icon={iconRight} onPress={this.state.rightAction.onClick} />
:
<ReactNative.TouchableOpacity style={BoxHeaderTouchable}>
<ReactNative.View>
</ReactNative.View>
</ReactNative.TouchableOpacity>
}
</ReactNative.View >
);
}
}
My request for the change icon in other page (example: geolocation)
import React, { Component } from 'react';
import ReactNative from 'react-native';
import { styles, Container, Text } from './labsoft/labsoft.ui';
import App from "./app";
import { BasicPageProperties, BasicPageState, BasicPage } from './interfaces/generics/basicPage';
export interface GeolocationPageProperties extends BasicPageProperties {
}
export interface GeolocationPageState {
latitude: any,
longitude: any,
address: any,
error: any
}
export default class GeolocationPage extends BasicPage<GeolocationPageProperties, GeolocationPageState> {
constructor(props: GeolocationPageProperties) {
super(props);
this.state = {
latitude: null,
longitude: null,
address: null,
error: null,
};
}
componentWillMount() {
this.app.header.setRightAction({
icon: 'star',
onClick: () => { }
})
}
componentDidMount() {
navigator.geolocation.getCurrentPosition(
(position) => {
console.log(position);
this.setState({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
address: "",
error: null,
});
console.log('http://maps.googleapis.com/maps/api/geocode/json?latlng=' + position.coords.latitude + ',' + position.coords.longitude + '&sensor=true');
// fetch('http://maps.googleapis.com/maps/api/geocode/json?latlng=' + position.coords.latitude + ',' + position.coords.longitude + '&sensor=true')
// .then((response) => response.json())
// .then((data) => {
// this.setState({
// latitude: position.coords.latitude,
// longitude: position.coords.longitude,
// address: data.results[0].formatted_address,
// error: null,
// });
// })
// .catch((error) => {
// console.error(error);
// });
},
(error) => this.setState({ error: error.message }),
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 1000 },
);
}
render() {
return (
<Container>
<Text>Latitude: {this.state.latitude}</Text>
<Text>Longitude: {this.state.longitude}</Text>
<Text>Endereço: {this.state.address}</Text>
{this.state.error ? <Text>Error: {this.state.error}</Text> : null}
</Container>
);
}
}
And other code, for request change icon, but not working
import React, { Component } from 'react';
import ReactNative from 'react-native';
import { Container, Text, Button } from './labsoft/labsoft.ui';
import App from "./app";
import { BasicPageProperties, BasicPageState, BasicPage } from './interfaces/generics/basicPage';
export interface MainProperties extends BasicPageProperties {
}
export interface MainState extends BasicPageState {
}
export default class MainPage extends BasicPage<MainProperties, MainState> {
constructor(props: MainProperties) {
super(props);
}
render() {
return (
<Container>
<Button title="aaa" onPress={() => this.app.openDrawer()} />
<Button title="change right action"
onPress={() => {
this.app.header.setRightAction({
icon: "bars",
onClick: () => {
alert("star");
}
})
}} />
</Container>
);
}
}
When navigator render other page, i set null in header icon.
it's working
Related
I want to slide out a React component when it umounts. I am using CSSTransition for the animation which works great for mounting, but not unmounting. Somehow I need to delay the unmount process. All the off-the-shelf solutions sadly do not work for me. I am removing an element by doing a post request and then actually removing it in the UI with a SignalR callback.
To make my sequence more clear, I created a sequence diagram:
This is my code right now:
Board.tsx
import React from 'react';
import { Config } from 'util/config';
import { container } from 'tsyringe';
import { AppState } from 'store';
import { connect } from 'react-redux';
import { BoardState } from 'store/board/types';
import { BoardHubService } from 'services/hubs/boardHub.service';
import { BoardElementViewModel } from 'models/BoardElementViewModel';
import { BoardViewModel } from 'models/BoardViewModel';
import { BoardElement } from './boardElement/boardElement';
import { HttpService } from 'services/http.service';
import { setActiveBoard } from 'store/board/actions';
import './board.scss'
import { mapToType } from 'helpers/helpers';
import { TransitionGroup } from 'react-transition-group';
interface BoardProps {
activeBoardState: BoardState;
setActiveBoard: typeof setActiveBoard;
}
interface LocalBoardState {
boardElements: Array<BoardElementViewModel>
}
class Board extends React.Component<BoardProps, LocalBoardState> {
private config: Config;
private httpService: HttpService;
private boardHubService: BoardHubService;
constructor(props: any) {
super(props);
this.config = container.resolve(Config);
this.boardHubService = container.resolve(BoardHubService);
this.httpService = container.resolve(HttpService);
this.state = {
boardElements: []
}
}
async componentDidMount() {
// If there was any active board on page load...
if (this.props.activeBoardState.boardId) {
await this.loadBoardElements();
}
this.boardHubService.getConnection().on('SwitchedBoard', (response: BoardViewModel | null) => {
console.log(response);
this.setState({
boardElements: (response) ? response.elements : []
});
this.updateSiteTitle(response);
});
this.boardHubService.getConnection().on('ReceiveElement', (response: BoardElementViewModel) => {
let elements = this.state.boardElements;
elements.unshift(response);
this.setState(() => ({
boardElements: elements
}))
});
this.boardHubService.getConnection().on('RemoveElement', (response: string) => {
let elements = this.state.boardElements;
let element = mapToType<BoardElementViewModel>(elements.find(x => x.id === response));
elements.splice(elements.indexOf(element), 1);
this.setState(() => ({
boardElements: elements
}))
});
}
/**
* Load the elements from the board that was already active on page load.
*/
private async loadBoardElements() {
await this.httpService.getWithAuthorization<Array<BoardElementViewModel>>(`/boards/${this.props.activeBoardState.boardId}/elements`)
.then((response: Array<BoardElementViewModel>) => {
this.setState({
boardElements: response
});
})
.catch((e) => console.warn(e));
}
private updateSiteTitle(board: BoardViewModel | null) {
if (board != null) {
document.title = `${board.name} | ${this.config.siteName}`;
}
else {
document.title = this.config.siteName;
}
}
render() {
return (
<>
{this.props.activeBoardState.boardId != null
?
<div className="board-elements">
{this.state.boardElements.map((element: BoardElementViewModel, index) => {
return (
<BoardElement
key={index}
id={element.id}
// TODO: Use number from server
number={element.elementNumber}
user={element.user}
direction={element.direction}
note={element.note}
imageId={element.imageId}
createdAt={element.createdAt}
/>
)
})}
</div>
:
<div className="select-board-instruction">
<h1>Please select or create a board.</h1>
</div>
}
</>
)
}
}
const mapStateToProps = (state: AppState) => ({
activeBoardState: state.activeBoard
});
export default connect(mapStateToProps, { setActiveBoard })(Board);
BoardElement.tsx
import React from 'react';
import { UserViewModel } from 'models/UserViewModel';
import { Direction } from 'models/Direction';
import './boardElement.scss';
import { dateToReadableString } from 'helpers/helpers';
import { Config } from 'util/config';
import { container } from 'tsyringe';
import { HttpService } from 'services/http.service';
import $ from 'jquery'
import { CSSTransition } from 'react-transition-group';
interface BoardElementProps {
id: string;
number: number;
user: UserViewModel;
// TODO: Use direction Enum
direction?: Direction;
note?: string;
imageId?: string;
createdAt: Date;
}
interface BoardElementState {
show: boolean;
}
export class BoardElement extends React.Component<BoardElementProps, BoardElementState> {
private config: Config;
private httpService: HttpService;
private ref: any;
constructor(props: BoardElementProps) {
super(props);
this.state = {
show: false,
}
this.config = container.resolve(Config);
this.httpService = container.resolve(HttpService);
}
getReadableDirection(direction: Direction) {
// TODO: Richtingen vertalen
switch (direction) {
case Direction.North: return 'Noord';
case Direction.NorthEast: return 'Noordoost';
case Direction.East: return 'Oost';
case Direction.SouthEast: return 'Zuidoost';
case Direction.South: return 'Zuid';
case Direction.SouthWest: return 'Zuidwest';
case Direction.West: return 'West';
case Direction.NorthWest: return 'Noordwest';
}
}
removeElement() {
this.httpService.deleteWithAuthorization(`/boards/elements/${this.props.id}`).then(() => {
}, (error) => {
console.warn(error);
});
}
componentDidMount() {
setTimeout(() => {
this.setState(() => ({
show: true
}));
}, 500);
}
componentWillUnmount() {
this.setState(() => ({
show: false
}));
}
render() {
return (
<CSSTransition in={this.state.show} timeout={200} classNames={{
enter: 'animation-height',
enterDone: 'animation-height',
exit: ''
}}>
<div className="animation-wrapper animation-height-0" >
<div className="board-element" >
<div className="board-element-header">
<span className="board-element-number">{this.props.number}</span>
<span className="board-element-creator">{this.props.user.username}</span>
<i className="fas fa-trash ml-auto delete-icon" onClick={() => this.removeElement()}></i>
</div>
<div className="board-element-body">
{this.props.imageId
? <img className="board-element-image" src={`${this.config.apiUrl}/content/${this.props.imageId}`} />
: <p className="board-element-message">{this.props.note}</p>
}
</div>
<div className="board-element-footer">
{this.props.direction &&
<div className="board-element-direction">
<i className="fas fa-location-arrow direction mr-2"></i>{this.getReadableDirection(this.props.direction)}
</div>
}
<time className="board-element-timestamp" dateTime={this.props.createdAt.toString()}>{dateToReadableString(this.props.createdAt)}</time>
</div>
</div>
</div>
</CSSTransition >
)
}
}
For better illustration take a look at this GIF:
https://gyazo.com/3c933851ecec39029f25d4df3a136c2a
That is using jQuery in another project of mine. That is what I want to achive in React.
You can delay unmounting the component. Write a hoc and use a setTimeout. Maintain a state say shouldRender.
hoc
function delayUnmounting(Component) {
return class extends React.Component {
state = {
shouldRender: this.props.isMounted
};
componentDidUpdate(prevProps) {
if (prevProps.isMounted && !this.props.isMounted) {
setTimeout(
() => this.setState({ shouldRender: false }),
this.props.delayTime
);
} else if (!prevProps.isMounted && this.props.isMounted) {
this.setState({ shouldRender: true });
}
}
render() {
return this.state.shouldRender ? <Component {...this.props} /> : null;
}
};
}
usage
function Box(props) {
return (
<BoxWrapper isMounted={props.isMounted} delay={props.delay}>
✨🎶✨🎶✨🎶✨🎶✨
</BoxWrapper>
);
}
const DelayedComponent = delayUnmounting(Box);
See complete code in the demo
Read this article on medium
Error
TypeError: Cannot read property 'length' of undefined
My App component is making use of import get from 'lodash.get' https://lodash.com/docs/4.17.11#get
I'm using get inside my render function like so:
const getLabel = (listings, label) => {
const componentsMap = {
Deliveries: Delivery,
Dispensaries: Dispensary,
Doctors: Doctor
};
const DynamicIcon = componentsMap[label];
if (get(listings, 'listings').length) {
return (
<div key={label}>
<DynamicIcon fill={DARK_GRAY} /> <strong> {label} </strong>
</div>
);
}
return <div />;
};
App.test.js
import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { AppJest } from './App'
import listingsMock from '../__test__/mocks/listings-mock.json';
// Mock the services.
const mockLocate = jest.fn();
const mockDisplayListing = jest.fn();
jest.mock('../actions', () => ({
locate: () => mockLocate(),
displayListing: () => mockDisplayListing()
}));
describe('<App /> component', () => {
describe('when rendering', () => {
const wrapper = shallow(<AppJest
listings={listingsMock}
locate={mockLocate}
displayListing={mockDisplayListing}
/>);
it('should render a component matching the snapshot', () => {
const tree = toJson(wrapper);
expect(tree).toMatchSnapshot();
expect(wrapper).toHaveLength(1);
});
});
});
I assumed it was because I wasn't mocking listings and passing it into the props of the shallow wrapper, but I added the mock.
listings-mock.json
{
"bottom_right": {
"latitude": 32.618865,
"longitude": -96.555516
},
"id": 1390,
"latitude": 32.78143692016602,
"listings": [
{
"avatar_image": {
"small_url": "https://images.weedmaps.com/deliveries/000/028/448/avatar/square_fill/1510581750-1507658638-Knox_Medical_Logo.png"
},
"city": "Dallas",
"distance": 2,
"id": 28448,
"license_type": "medical",
"name": "Knox Medical (Delivery Now Available)",
"online_ordering": {
"enabled_for_pickup": false,
"enabled_for_delivery": false
},
"package_level": "listing_plus",
"rating": 5,
"region_id": 1390,
"retailer_services": [
"delivery"
],
"slug": "knox-medical-dallas",
"state": "TX",
"static_map_url": "https://staticmap.weedmaps.com/static_map/13/32.7736/-96.795108/402/147/map.png",
"wmid": 459977538
}
],
"longitude": -96.7899169921875,
"name": "Dallas",
"region_path": "united-states/texas/dallas",
"slug": "dallas",
"top_left": {
"latitude": 33.016492,
"longitude": -96.999319
}
}
App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import get from 'lodash.get';
import { locate, displayListing } from '../actions';
import Header from './header';
import Hero from './hero';
import Ripple from './partials/ripple';
import ListingCards from './listing_cards';
import Delivery from '../icons/delivery';
import Dispensary from '../icons/dispensary';
import Doctor from '../icons/doctor';
import { DARK_GRAY } from '../constants/colors';
import {
AppWrapper,
AppContent,
ListingGroups,
} from './styles';
const regionTypes = ['delivery', 'dispensary', 'doctor'];
const regionLabels = {
delivery: 'Deliveries',
dispensary: 'Dispensaries',
doctor: 'Doctors',
};
export class App extends Component {
constructor(props) {
super(props);
this.state = {
loadingTimer: 0,
isLocatingStarted: false,
geoCoords: null,
width: 0
};
this.locateMe = this.locateMe.bind(this);
this.gotoListing = this.gotoListing.bind(this);
}
componentDidMount() {
// Fetch geolocation ahead of time.
navigator.geolocation.getCurrentPosition(position =>
this.setState({ geoCoords: position.coords }));
this.updateWindowDimensions();
window.addEventListener("resize", this.updateWindowDimensions);
}
componentWillUnmount() {
window.removeEventListener("resize", this.updateWindowDimensions);
}
updateWindowDimensions = () => this.setState({ width: window.innerWidth });
locateMe() {
console.log('locateMe')
const { dispatch } = this.props;
const { geoCoords } = this.state;
if (navigator.geolocation && !geoCoords) {
navigator.geolocation.getCurrentPosition(position =>
dispatch(locate(position.coords)));
} else {
dispatch(locate(geoCoords));
}
this.setState({ isLocatingStarted: true });
};
gotoListing(listing) {
const { dispatch } = this.props;
dispatch(displayListing(listing));
const link = `/listing/${listing.wmid}`;
this.props.history.push(link);
}
render() {
const { isLocating, location, regions, error } = this.props;
const { isLocatingStarted, width } = this.state;
const { state_abv: state } = location !== null && location;
const isLoading = isLocatingStarted && isLocating;
const getLabel = (listings, label) => {
const componentsMap = {
Deliveries: Delivery,
Dispensaries: Dispensary,
Doctors: Doctor
};
const DynamicIcon = componentsMap[label];
if (get(listings, 'listings').length) {
return (
<div key={label}>
<DynamicIcon fill={DARK_GRAY} /> <strong> {label} </strong>
</div>
);
}
return <div />;
};
return (
<AppWrapper>
<Header history={this.props.history} />
<Hero
location={location}
isLocating={isLocating}
locateMe={this.locateMe}
/>
{ isLoading ? <Ripple /> :
<AppContent>
{error && <div> {error.message} </div>}
{regions && (
<React.Fragment>
{regionTypes.map(regionType => (
<ListingGroups key={regionType}>
<h2>
{getLabel(regions[regionType], regionLabels[regionType])}
</h2>
<ListingCards
listings={get(regions[regionType], 'listings')}
state={state}
isMobileSize={width < 769}
gotoListing={this.gotoListing}
/>
</ListingGroups>
))}
</React.Fragment>
)}
</AppContent>
}
</AppWrapper>
);
}
}
const mapStateToProps = state => state.location;
App.propTypes = {
isLocating: PropTypes.bool.isRequired,
location: PropTypes.object,
regions: PropTypes.object,
dispatch: PropTypes.any,
error: PropTypes.object,
};
App.defaultProps = {
isLocating: false,
location: {},
regions: {},
error: {},
};
export const AppJest = App
export default connect(mapStateToProps)(App);
Ah I needed to finish adding in all the props to the wrapper component:
import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { AppJest } from './App'
import Header from './header';
import Hero from './hero';
import Ripple from './partials/ripple';
import listingsMock from '../__test__/mocks/listings-mock.json';
// Mock the services.
const mockLocate = jest.fn();
const mockDisplayListing = jest.fn();
const mockGeolocation = {
getCurrentPosition: jest.fn(),
watchPosition: jest.fn()
};
global.navigator.geolocation = mockGeolocation;
jest.mock('../actions', () => ({
locate: () => mockLocate(),
displayListing: () => mockDisplayListing()
}));
describe('<App /> component', () => {
describe('when rendering', () => {
const wrapper = shallow(<AppJest
navigator={mockGeolocation}
isLocating={false}
location={null}
regions={null}
dispatch={null}
error={null}
listings={listingsMock}
locate={mockLocate}
displayListing={mockDisplayListing}
/>);
it('should render a component matching the snapshot', () => {
const tree = toJson(wrapper);
expect(tree).toMatchSnapshot();
expect(wrapper).toHaveLength(1);
expect(wrapper.find(Header)).toHaveLength(1);
expect(wrapper.find(Hero)).toHaveLength(1);
});
});
});
Required props:
App.propTypes = {
isLocating: PropTypes.bool.isRequired,
location: PropTypes.object,
regions: PropTypes.object,
dispatch: PropTypes.any,
error: PropTypes.object,
};
App.defaultProps = {
isLocating: false,
location: {},
regions: {},
error: {},
};
I have the following Higher Order Component to create a loading on my components:
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import Spinner from 'components/spinner';
const WithSpinner = (WrappedComponent) => {
return class SpinnerWrapper extends Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render (){
const { loading } = this.props;
return (
<div>
{loading ?
<Spinner />
:
<WrappedComponent {...this.props}>{this.props.children}</WrappedComponent>}
</div>)
}
}
SpinnerWrapper.propTypes = {
loading: PropTypes.bool,
};
SpinnerWrapper.defaultProps = {
loading: true,
};
return SpinnerWrapper;
};
export default WithSpinner;
and I have the following component who is wrapping in WithSpinner
const updateChartSeries = answersSummary => ({
chartSeries: TasksTransforms.fromAnswerSummaryToClickEffectivenessChart(answersSummary),
transposed: false,
viewOptions: { type: 'pie', stacked: false, column: false },
});
class ClickEffectivenessChart extends Component {
constructor(props) {
super(props);
this.state = {
chartSeries: [],
viewOptions: {
type: 'pie',
stacked: false,
column: false,
},
title: 'Click Effectiveness',
};
}
componentWillReceiveProps({ answersSummary }) {
this.setState(updateChartSeries(answersSummary));
}
handleChartType = (type = 'pie') => {
switch (type) {
case 'pie':
this.setState({ viewOptions: { type: 'pie', stacked: false, column: false } });
break;
case 'bar':
this.setState({ viewOptions: { type: 'bar', stacked: false, column: false } });
break;
case 'column':
this.setState({ viewOptions: { type: 'bar', stacked: false, column: true } });
break;
default:
break;
}
}
optionsDropdown = () => {
const options = [
{ onClick: () => this.handleChartType('pie'), text: 'Pie' },
{ onClick: () => this.handleChartType('bar'), text: 'Bar' },
{ onClick: () => this.handleChartType('column'), text: 'Column' },
];
return options;
}
render() {
const { chartSeries, viewOptions, title } = this.state;
return (
chartSeries.length > 0 &&
<div className="content" >
<h4>{title}</h4>
<div className="box">
<EffectivenessChart type={viewOptions.type} series={chartSeries} title={title} optionMenu={this.optionsDropdown()} stacked={viewOptions.stacked} column={viewOptions.column} transposed />
</div>
</div>
);
}
}
ClickEffectivenessChart.propTypes = {
answersSummary: PropTypes.object, //eslint-disable-line
};
ClickEffectivenessChart.defaultProps = {
answersSummary: {},
};
export default WithSpinner(ClickEffectivenessChart);
And sometimes the componentWillReceiveProps it no trigger and I can see why. I'm assuming that my WithSpinner controller have a problem but I can see what is the problem. The same behavior happen in all components wrapped with the WithSpinner controller
Any help please?
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Heatmap from 'components/heatmap';
import ClickEffectivenessChart from './click_effectiveness_chart';
import ClickEffectivenessTable from './click_effectiveness_table';
import AreaChart from './area_chart';
import SctHeatmap from './sct_heatmap';
class SctAnswersSummary extends Component {
componentDidMount() {
if (this.props.questionId) { this.fetchData(); }
}
componentDidUpdate(prevProps) {
if (prevProps.questionId !== this.props.questionId) { this.fetchData(); }
}
fetchData = () => {
const {
datacenterId, accountId, projectId, questionId,
} = this.props;
this.props.actions.questionFetchRequest(datacenterId, accountId, projectId, questionId);
this.props.actions.answersSummaryFetchRequest(datacenterId, accountId, projectId, questionId);
this.props.actions.answersSctClicksFetchRequest(datacenterId, accountId, projectId, questionId);
}
render() {
const { imageUrl, clicks, questionId, imageWidth, imageHeight } = this.props.answerSctClicks;
const { answersSummaryLoading, answersSctLoading, answersSummary } = this.props;
return (
<div className="content">
<div className="content" >
<SctHeatmap imageUrl={imageUrl} clicks={clicks} questionId={questionId} imageWidth={imageWidth} imageHeight={imageHeight} loading={answersSctLoading} />
<br />
<ClickEffectivenessChart answersSummary={answersSummary} loading={answersSummaryLoading} />
<br />
<AreaChart answersSummary={answersSummary} loading={answersSummaryLoading} />
<br />
<ClickEffectivenessTable answersSummary={answersSummary} loading={answersSummaryLoading} />
</div>
</div>
);
}
}
SctAnswersSummary.propTypes = {
datacenterId: PropTypes.string,
accountId: PropTypes.string,
projectId: PropTypes.string,
questionId: PropTypes.string,
questionSct: PropTypes.object, // eslint-disable-line react/forbid-prop-types
actions: PropTypes.shape({
questionSctFetchRequest: PropTypes.func,
}).isRequired,
answersSummaryLoading: PropTypes.bool,
answersSctLoading: PropTypes.bool,
};
SctAnswersSummary.defaultProps = {
datacenterId: null,
accountId: null,
projectId: null,
questionId: null,
questionSct: {},
answersSummaryLoading: true,
answersSctLoading: true,
};
export default SctAnswersSummary;
It's because your HOC is a function stateless component, meaning that it doesn't have any lifecycle events. You need to convert WithSpinner to a class component and then the lifecycle events should flow through to your wrapped component:
const WithSpinner = (WrappedComponent) => {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render() {
return <div>
{this.props.loading ?
<Spinner />
:
<WrappedComponent {...this.props}>
{this.props.children}
</WrappedComponent>}
</div>
}
}
}
I have to pass data from MyWeather to MyWeatherData and it occurs that props does not pass into child component, when I use componentDidMount in MyWeatherData. Now I'm using componentDidUpdate in MyWeatherData and it works, but it produces an error: Can only update a mounted or mounting component. Please check the code for the ... component. Does anybody knows how to correct this?
When I pass: <MyWeatherData lat="53.0038" lon="20.0458"/> it works with ComponentDidMount.
import React, {Component} from "react";
import MyWeatherData from "./MyWeatherData";
export default class MyWeather extends Component {
constructor() {
super();
this.state = {
latitude: "",
longitude: ""
}
this.getMyLocation = this
.getMyLocation
.bind(this)
}
componentDidMount() {
this.getMyLocation()
}
getMyLocation() {
const location = window.navigator && window.navigator.geolocation
if (location) {
location.getCurrentPosition(pos => {
this.setState({latitude: pos.coords.latitude, longitude: pos.coords.longitude})
}, (error) => {
this.setState({latitude: 'err-latitude', longitude: 'err-longitude'})
})
}
}
render() {
const {latitude, longitude} = this.state;
return (
<div>
<MyWeatherData lat={latitude} lon={longitude}/>
</div>
)
}
}
import React, {Component} from "react";
import axios from "axios";
export default class MyWeatherData extends Component {
constructor() {
super();
this.state = {
descriptionMain: "",
description: "",
temperature: null,
weatherIcon: "",
name: ""
}
}
componentDidUpdate = () => {
this.getMyWeather();
}
getMyWeather = () => {
const lat = this.props.lat;
const lon = this.props.lon;
const API_KEY = "e6f4d816d3ade705ec1d8d9701b61e14";
const weatherURL = `https://api.openweathermap.org/data/2.5/weather?APPID=${API_KEY}&units=metric&lat=${lat}&lon=${lon}`;
axios
.get(weatherURL)
.then(res => {
this.setState({descriptionMain: res.data.weather[0].main, description: res.data.weather[0].description, temperature: res.data.main.temp, weatherIcon: res.data.weather[0].icon, name: res.data.name});
})
.catch(error => {
console.log(error);
});
}
render() {
const {descriptionMain, description, temperature, weatherIcon, name} = this.state;
return (
<div>
<h2>Weather for: {name}</h2>
<h4>Sky: {description}</h4>
<h5>Description: {descriptionMain}</h5>
<span className="temperature">{temperature}
°C</span>
{weatherIcon
? (<img
src={`http://openweathermap.org/img/w/${weatherIcon}.png`}
alt={`${description}`}/>)
: null}
</div>
)
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
When getting the information in componentDidMount in the childComponent, you props are passed but since the componentDidMount of the parent is not run by that time the updated props are not available, you should add that part of the code in componentWillReceiveProps which is executed whenever the props change or the parent component rerenders
import React, {Component} from "react";
import MyWeatherData from "./MyWeatherData";
export default class MyWeather extends Component {
constructor() {
super();
this.state = {
latitude: "",
longitude: ""
}
this.getMyLocation = this
.getMyLocation
.bind(this)
}
componentDidMount() {
this.getMyLocation()
}
getMyLocation() {
const location = window.navigator && window.navigator.geolocation
if (location) {
location.getCurrentPosition(pos => {
this.setState({latitude: pos.coords.latitude, longitude: pos.coords.longitude})
}, (error) => {
this.setState({latitude: 'err-latitude', longitude: 'err-longitude'})
})
}
}
render() {
const {latitude, longitude} = this.state;
return (
<div>
<MyWeatherData lat={latitude} lon={longitude}/>
</div>
)
}
}
import React, {Component} from "react";
import axios from "axios";
export default class MyWeatherData extends Component {
constructor() {
super();
this.state = {
descriptionMain: "",
description: "",
temperature: null,
weatherIcon: "",
name: ""
}
}
componentWillReceiveProps(nextProps){
if(nextProps.lat !== this.props.lat || nextProps.lon !== this.props.lon) {
this.getMyWeather(nextProps);
}
}
getMyWeather = (props) => {
const lat = props.lat;
const lon = props.lon;
const API_KEY = "e6f4d816d3ade705ec1d8d9701b61e14";
const weatherURL = `https://api.openweathermap.org/data/2.5/weather?APPID=${API_KEY}&units=metric&lat=${lat}&lon=${lon}`;
axios
.get(weatherURL)
.then(res => {
this.setState({descriptionMain: res.data.weather[0].main, description: res.data.weather[0].description, temperature: res.data.main.temp, weatherIcon: res.data.weather[0].icon, name: res.data.name});
})
.catch(error => {
console.log(error);
});
}
render() {
const {descriptionMain, description, temperature, weatherIcon, name} = this.state;
return (
<div>
<h2>Weather for: {name}</h2>
<h4>Sky: {description}</h4>
<h5>Description: {descriptionMain}</h5>
<span className="temperature">{temperature}
°C</span>
{weatherIcon
? (<img
src={`http://openweathermap.org/img/w/${weatherIcon}.png`}
alt={`${description}`}/>)
: null}
</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/