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 {
text-align: center;
body {
background-color: #111;
}
.calculator {
&__points {
color: white;
text-align: center;
}
}
.trees {
display: flex;
justify-content: center;
}
.tree {
position: relative;
min-width: 300px;
height: 600px;
border: 1px solid black;
background-size: cover;
background-position: center;
min-width: 300px;
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 { TalentTree } from './TalentTree'
import {
@@ -12,48 +12,56 @@ interface Props {
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
// Calculator doesn't need to know about URL params
export class Calculator extends React.PureComponent<Props> {
static whyDidYouRender = true
export const Calculator: React.FC<Props> = ({ selectedClass }) => {
const [knownTalents, setKnownTalents] = useState(initMap)
state = {
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]
setKnownTalents(knownTalents =>
modifyTalentPoint(knownTalents, talent, modifier)
this.setState({
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 { NavLink } from 'react-router-dom'
import { classByName } from '../data/classes'
interface Props {
}
export const ClassPicker: React.FC<Props> = () => {
return (
<div className="">
Pick your class
</div>
<ul className="class-picker">
{Object.values(classByName).map((c) =>
<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 { Calculator } from './Calculator'
import { Link } from 'react-router-dom';
import { ClassPicker } from './ClassPicker'
interface Props {
pointString?: string // e.g. 2305302300--001
@@ -8,28 +8,28 @@ interface Props {
history: any
}
const ClassPicker = () => {
return <ul>
<li><Link to="/warlock">Warlock</Link></li>
<li><Link to="/paladin">Paladin</Link></li>
</ul>
}
export class IndexRoute extends React.PureComponent<Props> {
static whyDidYouRender = true
export const IndexRoute: React.FC<Props> = ({ match, history }) => {
const { selectedClass, pointString } = match.params
render() {
const { match, history } = this.props
const { selectedClass, pointString } = match.params
if (!selectedClass) {
history.replace('/warlock')
return null
if (!selectedClass) {
history.replace('/warlock')
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 {
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: 50px;
$col-distance-offset: 44px;
$col-distance: 56px;
&[data-col="0"] { left: $col-distance-offset; }
&[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 { Icon } from './Icon'
import classNames from 'classnames'
import { spells } from '../data/spells'
import { Map } from 'immutable';
import { getPointsInSpec, calcMeetsRequirements } from '../lib/tree';
@@ -14,8 +13,13 @@ interface Props {
knownTalents: Map<number, number>
/** Disabled override */
disabled?: boolean
onClick?: (e: any) => void
onRightClick?: (e: any) => void
onClick?: (talentId: number) => void
onRightClick?: (talentId: number) => void
}
const defaultProps: Partial<Props> = {
onClick: () => undefined,
onRightClick: () => undefined
}
const isAvailable = (talent: TalentData, specId: number, knownTalents: Map<number, number>): boolean => {
@@ -39,7 +43,7 @@ export const TalentSlot: FC<Props> = (props) => {
})
const handleContextMenu = (e) => {
if (props.onRightClick) props.onRightClick(e)
if (props.onRightClick) props.onRightClick(talent.id)
e.preventDefault()
return false
}
@@ -50,7 +54,7 @@ export const TalentSlot: FC<Props> = (props) => {
title={talent.ranks[0].toString()}
data-row={talent.row}
data-col={talent.col}
onClick={!disabled ? props.onClick : () => {}}
onClick={!disabled ? () => props.onClick(talent.id) : () => {}}
onContextMenu={handleContextMenu}
>
<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 { TalentSlot } from './TalentSlot';
import { getPointsInSpec } from '../lib/tree';
@@ -14,32 +14,40 @@ interface Props {
export const TalentTree: React.FC<Props> = ({ specId, knownTalents, availablePoints, onTalentPress }) => {
const talents = Object.values(talentsBySpec[specId])
const handleTalentPress = useCallback((talentId: number, modifier: 1 | -1) => {
return (e: MouseEvent) => {
onTalentPress(specId, talentId, modifier)
}
}, [specId, onTalentPress])
const handleClick = useCallback(
(talentId) => onTalentPress(specId, talentId, 1),
[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")`
}
return (
<div className="tree" style={style}>
<h2>{specNames[specId]} ({getPointsInSpec(specId, knownTalents)})</h2>
{talents.map((talent, index) =>
<TalentSlot
key={talent.id}
specId={specId}
talent={talent}
availablePoints={availablePoints}
knownTalents={knownTalents}
onClick={handleTalentPress(talent.id, 1)}
onRightClick={handleTalentPress(talent.id, -1)}
/>
)}
<div className="tree">
<div className="tree__header">
<h3>{specNames[specId]} ({getPointsInSpec(specId, knownTalents)})</h3>
</div>
<div className="tree__body" style={bodyStyle}>
{talents.map((talent) =>
<TalentSlot
key={talent.id}
specId={specId}
talent={talent}
availablePoints={availablePoints}
knownTalents={knownTalents}
onClick={handleClick}
onRightClick={handleRightClick}
/>
)}
</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';
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)
}
+1 -1
View File
@@ -95,7 +95,7 @@ export function parsePointString(str: string): List<List<number>> {
const list: Array<number[]> = []
const trees = str.split('-')
trees.map((stringForTree, index) => {
trees.forEach((stringForTree, index) => {
const points = stringForTree.split('').map(a => parseInt(a, 10))
list[index] = points
})