In my app, I'm trying to use Popper to create a tooltip over every element in the app.
(Usually, I would only show a single tooltip, but for a presentation I want to show more than one).
I wrote this utility Component to attach tooltip directly to ref.
It works pretty well, but when I try to use it inside an [].map() like regular react components, I lose all my positioning:
https://bit.dev/bit/base/atoms/ref-tooltip?example=5e81d946443f4900195606b7
import React, { Component } from 'react';
import { RefTooltip } from '#bit/bit.base.atoms.ref-tooltip'
export default class ExampleUsage extends Component {
state = { ref: [] };
handleRef = (elem) => {
if (this.state.ref.some(x => x === elem)) return;
this.setState({ ref: [elem] });
}
render() {
return (
<div>
<span ref={this.handleRef}>target</span>
{ /*
* (!)
* This .map() breaks tooltip
*
*/ }
{this.state.ref.map((elem, idx) => (
<RefTooltip key={idx} targetElement={elem}>
"tooltip"
</RefTooltip>
))}
</div>
);
}
}
//ref-tooltip.tsx
import React, { Component } from 'react';
import classNames from 'classnames';
//#ts-ignore
import createRef from 'react-create-ref';
import { createPopper, Instance, Options } from '#popperjs/core';
import styles from './ref-tooltip.module.scss';
export type RefTooltipProps = {
targetElement?: HTMLElement;
popperOptions?: Partial<Options>;
} & React.HTMLAttributes<HTMLDivElement>;
export class RefTooltip extends Component<RefTooltipProps> {
private ref = createRef();
private popperInstance?: Instance;
componentWillUnmount() {
this.destroy();
}
componentDidUpdate(prevProps: RefTooltipProps) {
const nextProps = this.props;
if (prevProps.targetElement !== nextProps.targetElement) {
this.reposition(nextProps.targetElement);
}
}
private reposition = (targetElement?: HTMLElement) => {
const { popperOptions = popperDefaultOptions } = this.props;
const popperElement = this.ref.current;
if (!targetElement) {
this.destroy();
}
if (!targetElement || !popperElement) return;
this.popperInstance = createPopper(targetElement, popperElement, popperOptions);
};
private destroy() {
if (!this.popperInstance) return;
this.popperInstance.destroy();
this.popperInstance = undefined;
}
render() {
const { className, targetElement, ...rest } = this.props;
return (
<div
{...rest}
ref={this.ref}
className={classNames(styles.tooltipWrapper, className)}
data-ignore-component-highlight
/>
);
}
}
const popperDefaultOptions: Partial<Options> = {
placement: 'top',
modifiers: [
{
name: 'flip',
enabled: false,
},
],
};
Expected:
Actual:
I don't understand why the .map() breaks popper. At least for an array of 1, it should behave the same.
Any ideas why this isn't 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
I have been working on a React project for some time now. I have a board with elements on it. These elements contain images. I have a working version of it here: https://geoboard.luukwuijster.dev/ . This version is just in plain JavaScript and jQuery, but it does illustrate what I want to achieve in the new React version. When I upload an image (by pasting it using CTRL + V) I want it to be animated. It should slide in and slide back out. In the older version I am just using jQuery's slideIn and slideOut, but I am now wondering how I can achive this in React.
I have already tried the following things:
CSSTransition. I have tried using CSSTransition by setting the height of the element to zero and having a transition on height. This does not work because the height of the element is variable, so it should be auto. transition does not work on height: auto.
Animate.CSS I have tried putting a slideInDown on the element, but since Animcate.CSS is using Transform, this isn't working. The Element just pops in a couple pixels above and slides down.
jQuery's slideIn and slideOut. This is somewhat working, but just bugs out. I use ref to get the element, but when adding a new element to the array it animates the last element and not the first. It's also glitching a bit when animating.
This is my code:
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';
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) => {
this.setState({
boardElements: response.elements
});
console.log('SwitchedBoard', this.props.activeBoardState);
this.updateSiteTitle();
});
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() {
if (this.props.activeBoardState.boardId != null) {
document.title = `${this.props.activeBoardState.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 { CSSTransition, Transition } from 'react-transition-group';
import $ from 'jquery'
interface BoardElementProps {
id: string;
number: number;
user: UserViewModel;
// TODO: Use direction Enum
direction?: Direction;
note?: string;
imageId?: string;
createdAt: Date;
}
export class BoardElement extends React.Component<BoardElementProps> {
private config: Config;
private httpService: HttpService;
private ref: any;
constructor(props: BoardElementProps) {
super(props);
this.state = {
show: false,
height: 0
}
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.ref).slideUp(200);
setTimeout(() => {
this.httpService.deleteWithAuthorization(`/boards/elements/${this.props.id}`).then(() => {
}, (error) => {
console.warn(error);
});
}, 250);
}
componentDidMount() {
setTimeout(() => {
$(this.ref).slideDown(200);
}, 500);
}
render() {
let style: any = { display: "none" };
return (
<div ref={element => this.ref = element} style={style}>
<div className="board-element" data-element-id={this.props.id}>
<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>
)
}
}
UPDATE:
https://gyazo.com/6dfb3db8cbd29da0f4f52af33c545421
This clip might illustrate my problem better. Look closly at the number in the corner of the elements. When I paste a new image the animation is not applied to the new image, but rather the last image in the array.
I have copied the react-diagrams demo project and done some changes to the TSCustomNodeWidget.tsx, and when I call engine.repaintCanvas() it does not update the values I have set (whereas the name of the DefaultNodeModel does).
TSCustomNodeModel.ts
import { NodeModel, DefaultPortModel, DefaultNodeModel } from '#projectstorm/react-diagrams';
import { BaseModelOptions } from '#projectstorm/react-canvas-core';
export interface TSCustomNodeModelOptions extends BaseModelOptions {
color?: string;
value?: number;
name?: string;
}
export class TSCustomNodeModel extends DefaultNodeModel {
color: string;
name: string;
value: number;
constructor(options: TSCustomNodeModelOptions = {}) {
super({
...options,
type: 'ts-custom-node'
});
this.color = options.color || 'red';
this.value = options.value || 0;
this.name = options.name || 'name';
// setup an in and out port
this.addPort(
new DefaultPortModel({
in: true,
name: 'IN'
})
);
this.addPort(
new DefaultPortModel({
in: false,
name: 'OUT'
})
);
}
serialize() {
return {
...super.serialize(),
color: this.color
};
}
deserialize(event:any): void {
super.deserialize(event);
this.color = event.data.color;
}
}
TSCustomNodeFactory.tsx
import * as React from 'react';
import { TSCustomNodeModel } from './TSCustomNodeModel';
import { TSCustomNodeWidget } from './TSCustomNodeWidget';
import { AbstractReactFactory } from '#projectstorm/react-canvas-core';
import { DiagramEngine } from '#projectstorm/react-diagrams-core';
export class TSCustomNodeFactory extends AbstractReactFactory<TSCustomNodeModel, DiagramEngine> {
constructor() {
super('ts-custom-node');
}
generateModel(initialConfig:any) {
return new TSCustomNodeModel();
}
generateReactWidget(event:any): JSX.Element {
return <TSCustomNodeWidget engine={this.engine as DiagramEngine} node={event.model} />;
}
}
TSCustomNodeWidget.tsx
import * as React from 'react';
import { DiagramEngine, PortWidget } from '#projectstorm/react-diagrams-core';
import { TSCustomNodeModel } from './TSCustomNodeModel';
export interface TSCustomNodeWidgetProps {
node: TSCustomNodeModel;
engine: DiagramEngine;
}
export interface TSCustomNodeWidgetState {}
export class TSCustomNodeWidget extends React.Component<TSCustomNodeWidgetProps, TSCustomNodeWidgetState> {
constructor(props: TSCustomNodeWidgetProps) {
super(props);
this.state = {};
}
render() {
return (
<div className="custom-node">
<PortWidget engine={this.props.engine} port={this.props.node.getPort('IN')}>
<div className="circle-port" />
</PortWidget>
<PortWidget engine={this.props.engine} port={this.props.node.getPort('OUT')}>
<div className="circle-port" />
</PortWidget>
<div className="custom-node-color"
style={{ backgroundColor: this.props.node.color }}>
{this.props.node.value}
</div>
</div>
);
}
}
App.tsx
Setup
// create an instance of the engine with all the defaults
const engine = createEngine();
const state = engine.getStateMachine().getCurrentState();
if (state instanceof DefaultDiagramState) {
state.dragNewLink.config.allowLooseLinks = false;
}
const model = new DiagramModel();
engine.getNodeFactories().registerFactory(new TSCustomNodeFactory());
model.setGridSize(5);
engine.setModel(model);
Adding the node with button (repaintCanvas works)
function addConstNode() {
const node = new TSCustomNodeModel({
name: "CONST",
value: 0
});
node.setPosition(50, 50);
model.addAll(node);
engine.repaintCanvas(); //This works
return node;
}
Updating the node (repaintCanvas does not work)
currentNode.value = value;
...
engine.repaintCanvas();
I had a similar issue, where changing nodes would not update canvas immediately, and I managed to get around it by forcing the react component to re-render by doing this:
function useForceUpdate(){
const [, setValue] = useState(0);
return () => setValue(value => ++value);
}
const MyComponent = () => {
const forceUpdate = useForceUpdate();
// do some update to diagram
...
forceUpdate();
};
Can't remember if I also had to call engine.repaintCanvas() also or not.
Bit late to the party but hope this helps.
This is my Accordion component
import React, {Component, Fragment} from 'react';
import ArrowTemplate from "./ArrowTemplate";
import ContentTemplate from "./ContentTemplate";
class Accordion extends Component {
constructor(props) {
super(props);
this.state = {isAccordionExpanded: false};
this.foldAccordion = this.foldAccordion.bind(this);
this.expandAccordion = this.expandAccordion.bind(this);
}
expandAccordion() {
console.log(1);
this.setState({isAccordionExpanded: true});
}
foldAccordion() {
console.log(2);
this.setState({isAccordionExpanded: false});
}
render() {
const {state} = this;
const {isAccordionExpanded} = state;
if (isAccordionExpanded === false) {
return (
<Fragment>
<ArrowTemplate
aria={`aria-expanded="true"`}
onClick={this.expandAccordion}
direction={'down'}
color={'black'}
styles={'background:yellow'}
/>
</Fragment>
);
} else if (isAccordionExpanded === true) {
return (
<Fragment>
<ArrowTemplate
aria={`aria-expanded="true"`}
onClick={this.foldAccordion}
color={'black'}
direction={'up'}
/>
<ContentTemplate/>
</Fragment>
);
}
}
}
export default Accordion;
this is the ArrowTemplate
import React from "react";
import BlackDownArrowSVG from './svgs/black-down-arrow.svg';
import WhiteDownArrowSVG from './svgs/white-down-arrow.svg';
import styled from 'styled-components';
import PropTypes from 'prop-types';
ArrowTemplate.propTypes = {
color: PropTypes.string.isRequired,
direction: PropTypes.string.isRequired,
styles: PropTypes.string,
aria: PropTypes.string.isRequired,
};
function ArrowTemplate(props) {
const {color, direction, styles, aria} = props;
const StyledArrowTemplate = styled.img.attrs({
src: color.toLowerCase() === "black" ? BlackDownArrowSVG : WhiteDownArrowSVG,
aria,
})`
${direction.toLowerCase() === "up" ? "translate: rotate(180deg)" : ""}
${styles}
`;
return <StyledArrowTemplate/>;
}
export default ArrowTemplate;
And here are my related tests
describe("<Accordion/>",
() => {
let wrapper;
beforeEach(
() => {
wrapper = shallow(
<Accordion/>
);
}
);
it('should have one arrow at start',
function () {
expect(wrapper.find(ArrowTemplate)).toHaveLength(1);
}
);
it('should change state onClick',
function () {
wrapper.find(ArrowTemplate).simulate("click");
expect(wrapper.state().isAccordionExpanded).toEqual(true);
}
);
it('should call FoldAccordionMock onClick',
function () {
wrapper.setState({isAccordionExpanded: true});
wrapper.find(ArrowTemplate).simulate("click");
expect(wrapper.state().isAccordionExpanded).toEqual(false);
}
);
it('should display content if isAccordionExpanded = true',
function () {
wrapper.setState({isAccordionExpanded: true});
expect(wrapper.find(ContentTemplate).exists()).toEqual(true);
}
);
it('should hide content if isAccordionExpanded = false',
function () {
expect(wrapper.find(ContentTemplate).exists()).toEqual(false);
}
);
}
);
So the problem is that when I tun the tests .simulate(click) seems to work, and all tests pass. But when I click the component myself, nothing happens. Not even a console log. Changing the onClick to onClick={()=>console.log(1)} doesn't work either. Any ideas what's wrong?
StyledArrowTemplate inner component does not know anything about onClick.
ArrowTemplate doesn't know what onClick means, it's just another arbitrary prop to it.
But, if you do as #ravibagul91 said in their comment, you should pass down onClick again, StyledArrowTemplate might know what onClick means.
So just add <StyledArrowTemplate onClick={props.onClick}/>
Accordion Component
import React, { Component, Fragment } from "react";
import ArrowTemplate from "./ArrowTemplate";
import ContentTemplate from "./ContentTemplate";
class Accordion extends Component {
constructor(props) {
super(props);
this.state = { isAccordionExpanded: false };
}
toggleAccordian = () => {
console.log(1);
this.setState({ isAccordionExpanded: !isAccordionExpanded });
};
render() {
const { state } = this;
const { isAccordionExpanded } = state;
if (isAccordionExpanded) {
return (
<Fragment>
<ArrowTemplate
aria={`aria-expanded="true"`}
onClick={() => this.toggleAccordian()}
color={"black"}
direction={isAccordionExpanded ? "up" : "down"}
styles={`background:{isAccordionExpanded ? 'yellow' : ''}`}
/>
<ContentTemplate />
</Fragment>
);
}
}
}
export default Accordion;
I'm encountering this strange issue that I can figure out why is happing.
This should not be happening since the prop passed down to the History component has not been updated.
./components/History.js
...
const History = ({ previousLevels }) => {
return (
<ScrollView style={styles.container}>
{previousLevels.reverse().map(({ date, stressValue, tirednessValue }) => {
return (
<CardKBT
key={date}
date={date}
stressValue={stressValue}
tirednessValue={tirednessValue}
/>
)
})}
</ScrollView>
)
}
...
export default History
As can be seen in this code (below), the prop to the History is only updated once the user press Save.
App.js
import React from 'react'
import { View, ScrollView, StyleSheet } from 'react-native'
import { AppLoading, Font } from 'expo'
import Store from 'react-native-simple-store'
import { debounce } from 'lodash'
import CurrentLevels from './components/CurrentLevels'
import History from './components/History'
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
isLoadingComplete: false,
currentLevels: {
stressValue: 1,
tirednessValue: 1,
},
previousLevels: [],
}
this.debounceUpdateStressValue = debounce(this.onChangeStressValue, 50)
this.debounceUpdateTirednessValue = debounce(
this.onChangeTirednessValue,
50
)
}
async componentDidMount() {
const previousLevels = await Store.get('previousLevels')
if (previousLevels) {
this.setState({ previousLevels })
}
}
render() {
const { stressValue, tirednessValue } = this.state.currentLevels
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
...
/>
)
} else {
return (
<View style={{ flex: 1 }}>
<CurrentLevels
stressValue={stressValue}
onChangeStressValue={this.debounceUpdateStressValue}
tirednessValue={tirednessValue}
onChangeTirednessValue={this.debounceUpdateTirednessValue}
onSave={this.onSave}
/>
<History previousLevels={this.state.previousLevels} />
</View>
)
}
}
...
onChangeStressValue = stressValue => {
const { tirednessValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onChangeTirednessValue = tirednessValue => {
const { stressValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onSave = () => {
Store.push('previousLevels', {
date: `${new Date()}`,
...this.state.currentLevels,
}).then(() => {
Store.get('previousLevels').then(previousLevels => {
this.setState({
currentLevels: { stressValue: 1, tirednessValue: 1 },
previousLevels,
})
})
})
}
}
The component will re-render when one of the props or state changes, try using PureComponent or implement shouldComponentUpdate() and handle decide when to re-render.
Keep in mind, PureComponent does shallow object comparison, which means, if your props have nested object structure. It won't work as expected. So your component will re-render if the nested property changes.
In that case, you can have a normal Component and implement the shouldComponentUpdate() where you can tell React to re-render based on comparing the nested properties changes.