Prevent unnecessary re-renders

This commit is contained in:
Melvin Valster
2019-07-20 16:11:28 +02:00
parent 109955166d
commit eddb47e8b1
9 changed files with 174 additions and 111 deletions
+41 -8
View File
@@ -1,21 +1,54 @@
.App { body {
text-align: center; background-color: #111;
} }
.calculator { .calculator {
&__points {
color: white;
text-align: center;
}
} }
.trees { .trees {
display: flex; display: flex;
justify-content: center;
} }
.tree { .tree {
position: relative; position: relative;
min-width: 300px; min-width: 300px;
height: 600px;
border: 1px solid black;
background-size: cover;
background-position: center;
color: white; color: white;
margin-right: 1em;
&:last-child {
margin-right: 0;
}
&__header {
text-align: center;
}
&__body {
position: relative;
height: 520px;
border: 1px solid black;
background-size: cover;
background-position: center;
}
} }
.class-picker {
display: flex;
&__class {
margin-right: 2em;
}
// TODO: Make BEM
a {
&.active {
font-weight: bold;
}
}
}
+47 -39
View File
@@ -1,4 +1,4 @@
import React, { useState, useCallback, useEffect } from 'react' import React from 'react'
import { Map } from 'immutable' import { Map } from 'immutable'
import { TalentTree } from './TalentTree' import { TalentTree } from './TalentTree'
import { import {
@@ -12,48 +12,56 @@ interface Props {
selectedClass: string selectedClass: string
} }
const initMap = Map<number, number>() const EMPTY_TALENTS = Map<number, number>()
// TODO: Wrap in "IndexRoute" or something similar to take care of the url params export class Calculator extends React.PureComponent<Props> {
// Calculator doesn't need to know about URL params static whyDidYouRender = true
export const Calculator: React.FC<Props> = ({ selectedClass }) => { state = {
const [knownTalents, setKnownTalents] = useState(initMap) knownTalents: EMPTY_TALENTS
}
const handleTalentPress = useCallback((specId: number, talentId: number, modifier: 1 | -1) => { componentDidUpdate(prevProps: Props) {
if (prevProps.selectedClass !== this.props.selectedClass) {
this.setState({
knownTalents: EMPTY_TALENTS
})
}
}
handleTalentPress = (specId: number, talentId: number, modifier: 1 | -1) => {
const talent = talentsBySpec[specId][talentId] const talent = talentsBySpec[specId][talentId]
setKnownTalents(knownTalents => this.setState({
modifyTalentPoint(knownTalents, talent, modifier) knownTalents: modifyTalentPoint(this.state.knownTalents, talent, modifier)
})
}
render() {
const { selectedClass } = this.props
const { knownTalents } = this.state
const classData = classByName[selectedClass]
const availablePoints = calcAvailablePoints(knownTalents)
return (
<div className="calculator">
<div className="trees">
{classData.specs.map((specId) => (
<TalentTree
key={specId}
specId={specId}
availablePoints={availablePoints}
knownTalents={knownTalents}
onTalentPress={this.handleTalentPress}
/>
))}
</div>
<div className="calculator__points">
Points: {availablePoints}
</div>
</div>
) )
}, []) }
// Reset known talents when switching class
useEffect(() => {
setKnownTalents(initMap)
}, [selectedClass])
const classData = classByName[selectedClass]
const availablePoints = calcAvailablePoints(knownTalents)
return (
<div className="calculator">
<div className="trees">
{classData.specs.map((specId) => (
<TalentTree
key={specId}
specId={specId}
availablePoints={availablePoints}
knownTalents={knownTalents}
onTalentPress={handleTalentPress}
/>
))}
</div>
<div className="calculator__points">
Points: {availablePoints}
</div>
</div>
)
} }
(Calculator as any).whyDidYouRender = true
+9 -3
View File
@@ -1,12 +1,18 @@
import React from 'react' import React from 'react'
import { NavLink } from 'react-router-dom'
import { classByName } from '../data/classes'
interface Props { interface Props {
} }
export const ClassPicker: React.FC<Props> = () => { export const ClassPicker: React.FC<Props> = () => {
return ( return (
<div className=""> <ul className="class-picker">
Pick your class {Object.values(classByName).map((c) =>
</div> <li key={c.id} className="class-picker__class">
<NavLink to={`/${c.name.toLowerCase()}`}>{c.name}</NavLink>
</li>
)}
</ul>
) )
} }
+22 -22
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { Calculator } from './Calculator' import { Calculator } from './Calculator'
import { Link } from 'react-router-dom'; import { ClassPicker } from './ClassPicker'
interface Props { interface Props {
pointString?: string // e.g. 2305302300--001 pointString?: string // e.g. 2305302300--001
@@ -8,28 +8,28 @@ interface Props {
history: any history: any
} }
const ClassPicker = () => { export class IndexRoute extends React.PureComponent<Props> {
return <ul> static whyDidYouRender = true
<li><Link to="/warlock">Warlock</Link></li>
<li><Link to="/paladin">Paladin</Link></li>
</ul>
}
export const IndexRoute: React.FC<Props> = ({ match, history }) => { render() {
const { selectedClass, pointString } = match.params const { match, history } = this.props
const { selectedClass, pointString } = match.params
if (!selectedClass) { if (!selectedClass) {
history.replace('/warlock') history.replace('/warlock')
return null return null
}
return (
<div className="index">
<ClassPicker />
{selectedClass &&
<Calculator
selectedClass={selectedClass}
/>
}
</div>
)
} }
return (
<div className="index">
<ClassPicker />
{selectedClass &&
<Calculator selectedClass={selectedClass} />
}
</div>
)
} }
+12 -10
View File
@@ -1,3 +1,8 @@
$row-distance: 70px;
@mixin rowStyle($rowNr) {
top: 30px + (($rowNr) * 70px);
}
.talent { .talent {
position: absolute; position: absolute;
@@ -33,18 +38,15 @@
} }
$row-distance: 70px; @for $i from 0 through 6 {
&[data-row="#{$i}"] {
@include rowStyle($i)
}
}
&[data-row="0"] { top: $row-distance; }
&[data-row="1"] { top: $row-distance * 2; }
&[data-row="2"] { top: $row-distance * 3; }
&[data-row="3"] { top: $row-distance * 4; }
&[data-row="4"] { top: $row-distance * 5; }
&[data-row="5"] { top: $row-distance * 6; }
&[data-row="6"] { top: $row-distance * 7; }
$col-distance-offset: 30px; $col-distance-offset: 44px;
$col-distance: 50px; $col-distance: 56px;
&[data-col="0"] { left: $col-distance-offset; } &[data-col="0"] { left: $col-distance-offset; }
&[data-col="1"] { left: $col-distance-offset + ($col-distance * 1); } &[data-col="1"] { left: $col-distance-offset + ($col-distance * 1); }
+12 -6
View File
@@ -2,7 +2,6 @@ import './TalentSlot.scss'
import React, { FC } from 'react' import React, { FC } from 'react'
import { Icon } from './Icon' import { Icon } from './Icon'
import classNames from 'classnames' import classNames from 'classnames'
import { spells } from '../data/spells'
import { Map } from 'immutable'; import { Map } from 'immutable';
import { getPointsInSpec, calcMeetsRequirements } from '../lib/tree'; import { getPointsInSpec, calcMeetsRequirements } from '../lib/tree';
@@ -14,8 +13,13 @@ interface Props {
knownTalents: Map<number, number> knownTalents: Map<number, number>
/** Disabled override */ /** Disabled override */
disabled?: boolean disabled?: boolean
onClick?: (e: any) => void onClick?: (talentId: number) => void
onRightClick?: (e: any) => void onRightClick?: (talentId: number) => void
}
const defaultProps: Partial<Props> = {
onClick: () => undefined,
onRightClick: () => undefined
} }
const isAvailable = (talent: TalentData, specId: number, knownTalents: Map<number, number>): boolean => { const isAvailable = (talent: TalentData, specId: number, knownTalents: Map<number, number>): boolean => {
@@ -39,7 +43,7 @@ export const TalentSlot: FC<Props> = (props) => {
}) })
const handleContextMenu = (e) => { const handleContextMenu = (e) => {
if (props.onRightClick) props.onRightClick(e) if (props.onRightClick) props.onRightClick(talent.id)
e.preventDefault() e.preventDefault()
return false return false
} }
@@ -50,7 +54,7 @@ export const TalentSlot: FC<Props> = (props) => {
title={talent.ranks[0].toString()} title={talent.ranks[0].toString()}
data-row={talent.row} data-row={talent.row}
data-col={talent.col} data-col={talent.col}
onClick={!disabled ? props.onClick : () => {}} onClick={!disabled ? () => props.onClick(talent.id) : () => {}}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
<Icon name={talent.icon} /> <Icon name={talent.icon} />
@@ -62,4 +66,6 @@ export const TalentSlot: FC<Props> = (props) => {
) )
} }
(TalentSlot as any).whyDidYouRender = true TalentSlot.defaultProps = defaultProps
// ;(TalentSlot as any).whyDidYouRender = true
+29 -21
View File
@@ -1,4 +1,4 @@
import React, { MouseEvent, useCallback } from 'react' import React, { useCallback } from 'react'
import { Map } from 'immutable' import { Map } from 'immutable'
import { TalentSlot } from './TalentSlot'; import { TalentSlot } from './TalentSlot';
import { getPointsInSpec } from '../lib/tree'; import { getPointsInSpec } from '../lib/tree';
@@ -14,32 +14,40 @@ interface Props {
export const TalentTree: React.FC<Props> = ({ specId, knownTalents, availablePoints, onTalentPress }) => { export const TalentTree: React.FC<Props> = ({ specId, knownTalents, availablePoints, onTalentPress }) => {
const talents = Object.values(talentsBySpec[specId]) const talents = Object.values(talentsBySpec[specId])
const handleTalentPress = useCallback((talentId: number, modifier: 1 | -1) => { const handleClick = useCallback(
return (e: MouseEvent) => { (talentId) => onTalentPress(specId, talentId, 1),
onTalentPress(specId, talentId, modifier) [specId, onTalentPress]
} )
}, [specId, onTalentPress]) const handleRightClick = useCallback(
(talentId) => onTalentPress(specId, talentId, -1),
[specId, onTalentPress]
)
const style = { const bodyStyle = {
backgroundImage: `url("https://wow.zamimg.com/images/wow/talents/backgrounds/classic/${specId}.jpg")` backgroundImage: `url("https://wow.zamimg.com/images/wow/talents/backgrounds/classic/${specId}.jpg")`
} }
return ( return (
<div className="tree" style={style}> <div className="tree">
<h2>{specNames[specId]} ({getPointsInSpec(specId, knownTalents)})</h2> <div className="tree__header">
{talents.map((talent, index) => <h3>{specNames[specId]} ({getPointsInSpec(specId, knownTalents)})</h3>
<TalentSlot </div>
key={talent.id}
specId={specId} <div className="tree__body" style={bodyStyle}>
talent={talent} {talents.map((talent) =>
availablePoints={availablePoints} <TalentSlot
knownTalents={knownTalents} key={talent.id}
onClick={handleTalentPress(talent.id, 1)} specId={specId}
onRightClick={handleTalentPress(talent.id, -1)} talent={talent}
/> availablePoints={availablePoints}
)} knownTalents={knownTalents}
onClick={handleClick}
onRightClick={handleRightClick}
/>
)}
</div>
</div> </div>
) )
} }
(TalentTree as any).whyDidYouRender = true ;(TalentTree as any).whyDidYouRender = true
+1 -1
View File
@@ -5,7 +5,7 @@ import App from './App';
import * as serviceWorker from './serviceWorker'; import * as serviceWorker from './serviceWorker';
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
const whyDidYouRender = require('@welldone-software/why-did-you-render') const whyDidYouRender = require('@welldone-software/why-did-you-render/dist/no-classes-transpile/umd/whyDidYouRender.min.js')
whyDidYouRender(React) whyDidYouRender(React)
} }
+1 -1
View File
@@ -95,7 +95,7 @@ export function parsePointString(str: string): List<List<number>> {
const list: Array<number[]> = [] const list: Array<number[]> = []
const trees = str.split('-') const trees = str.split('-')
trees.map((stringForTree, index) => { trees.forEach((stringForTree, index) => {
const points = stringForTree.split('').map(a => parseInt(a, 10)) const points = stringForTree.split('').map(a => parseInt(a, 10))
list[index] = points list[index] = points
}) })