Ajout du lint pour les hooks React
Comme recommandé dans la documentation des hooks React, ajout des deux linters suivants : react-hooks/rules-of-hooks et react-hooks/exhaustive-deps Mise à jour des composants, en particulier les useEffect pour y spécifier toutes les dépendances.pull/671/head
parent
3fbd94bc65
commit
dbbb67ee7f
|
@ -1,7 +1,4 @@
|
|||
rules:
|
||||
linebreak-style:
|
||||
- 2
|
||||
- unix
|
||||
quotes:
|
||||
- 1 # While https://github.com/eslint/eslint/issues/9662#issuecomment-353958854 we don't enforce this
|
||||
- single
|
||||
|
@ -14,10 +11,13 @@ rules:
|
|||
react/jsx-no-target-blank: 0
|
||||
react/no-unescaped-entities: 0
|
||||
react/display-name: 1
|
||||
react-hooks/rules-of-hooks: error
|
||||
react-hooks/exhaustive-deps: warn
|
||||
parser: babel-eslint
|
||||
|
||||
plugins:
|
||||
- react
|
||||
- react-hooks
|
||||
- flowtype
|
||||
env:
|
||||
browser: true
|
||||
|
|
|
@ -123,6 +123,7 @@
|
|||
"eslint-config-prettier": "^4.0.0",
|
||||
"eslint-plugin-flowtype": "^3.2.1",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"eslint-plugin-react-hooks": "^2.0.1",
|
||||
"express": "^4.16.3",
|
||||
"file-loader": "^1.1.11",
|
||||
"flow-bin": "^0.92.0",
|
||||
|
|
|
@ -8,6 +8,7 @@ import Overlay from './Overlay'
|
|||
// Il suffit à la section d'appeler une fonction fournie en lui donnant du JSX
|
||||
export let AttachDictionary = dictionary => Decorated =>
|
||||
function withDictionary(props) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [{ explanation, term }, setState] = useState({
|
||||
term: null,
|
||||
explanation: null
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import classnames from 'classnames'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import NumberFormat from 'react-number-format'
|
||||
import { debounce } from '../../utils'
|
||||
import './CurrencyInput.css'
|
||||
|
@ -22,23 +22,27 @@ let currencyFormat = language => ({
|
|||
})
|
||||
|
||||
export default function CurrencyInput({
|
||||
value: valueArg,
|
||||
value: valueProp = '',
|
||||
debounce: debounceTimeout,
|
||||
onChange,
|
||||
language,
|
||||
className,
|
||||
...forwardedProps
|
||||
}) {
|
||||
const [currentValue, setCurrentValue] = useState(valueArg)
|
||||
const [initialValue] = useState(valueArg)
|
||||
// When the component is rendered with a new "value" argument, we update our local state
|
||||
useEffect(() => {
|
||||
setCurrentValue(valueArg)
|
||||
}, [valueArg])
|
||||
const nextValue = useRef(null)
|
||||
const [initialValue, setInitialValue] = useState(valueProp)
|
||||
const [currentValue, setCurrentValue] = useState(valueProp)
|
||||
const onChangeDebounced = useRef(
|
||||
debounceTimeout ? debounce(debounceTimeout, onChange) : onChange
|
||||
)
|
||||
// We need some mutable reference because the <NumberFormat /> component doesn't provide
|
||||
// the DOM `event` in its custom `onValueChange` handler
|
||||
const nextValue = useRef(null)
|
||||
|
||||
// When the component is rendered with a new "value" prop, we reset our local state
|
||||
if (valueProp !== initialValue) {
|
||||
setCurrentValue(valueProp)
|
||||
setInitialValue(valueProp)
|
||||
}
|
||||
|
||||
const handleChange = event => {
|
||||
// Only trigger the `onChange` event if the value has changed -- and not
|
||||
|
@ -61,19 +65,17 @@ export default function CurrencyInput({
|
|||
thousandSeparator,
|
||||
decimalSeparator
|
||||
} = currencyFormat(language)
|
||||
|
||||
// We display negative numbers iff this was the provided value (but we allow the user to enter them)
|
||||
// We display negative numbers iff this was the provided value (but we disallow the user to enter them)
|
||||
const valueHasChanged = currentValue !== initialValue
|
||||
|
||||
// Autogrow the input
|
||||
const valueLength = (currentValue || '').toString().length
|
||||
const valueLength = currentValue.toString().length
|
||||
const width = `${5 + (valueLength - 5) * 0.75}em`
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(className, 'currencyInput__container')}
|
||||
{...(valueLength > 5
|
||||
? { style: { width: `${5 + (valueLength - 5) * 0.75}em` } }
|
||||
: {})}>
|
||||
{...(valueLength > 5 ? { style: { width } } : {})}>
|
||||
{isCurrencyPrefixed && '€'}
|
||||
<NumberFormat
|
||||
{...forwardedProps}
|
||||
|
@ -84,10 +86,10 @@ export default function CurrencyInput({
|
|||
inputMode="numeric"
|
||||
onValueChange={({ value }) => {
|
||||
setCurrentValue(value)
|
||||
nextValue.current = value.toString().replace(/^\-/, '')
|
||||
nextValue.current = value.toString().replace(/^-/, '')
|
||||
}}
|
||||
onChange={handleChange}
|
||||
value={(currentValue || '').toString().replace('.', decimalSeparator)}
|
||||
value={currentValue.toString().replace('.', decimalSeparator)}
|
||||
/>
|
||||
{!isCurrencyPrefixed && <> €</>}
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,8 @@ import './PercentageField.css'
|
|||
export default function PercentageField({ onChange, value, debounce }) {
|
||||
const [localValue, setLocalValue] = useState(value)
|
||||
const debouncedOnChange = useCallback(
|
||||
debounce ? debounce(debounce, onChange) : onChange
|
||||
debounce ? debounce(debounce, onChange) : onChange,
|
||||
[debounce, onChange]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { findRuleByDottedName } from 'Engine/rules'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import {
|
||||
|
@ -15,17 +15,20 @@ export default function PeriodSwitch() {
|
|||
const initialPeriod = useSelector(
|
||||
state => state.simulation?.config?.situation?.période
|
||||
)
|
||||
const currentPeriod = situation.période
|
||||
useEffect(() => {
|
||||
!currentPeriod && updatePeriod(initialPeriod || 'année')
|
||||
}, [])
|
||||
const currentPeriod = situation.période
|
||||
const updatePeriod = toPeriod => {
|
||||
const needConversion = Object.keys(situation).filter(dottedName => {
|
||||
const rule = findRuleByDottedName(rules, dottedName)
|
||||
return rule?.période === 'flexible'
|
||||
})
|
||||
dispatch({ type: 'UPDATE_PERIOD', toPeriod, needConversion })
|
||||
}
|
||||
}, [currentPeriod, initialPeriod, updatePeriod])
|
||||
const updatePeriod = useCallback(
|
||||
toPeriod => {
|
||||
const needConversion = Object.keys(situation).filter(dottedName => {
|
||||
const rule = findRuleByDottedName(rules, dottedName)
|
||||
return rule?.période === 'flexible'
|
||||
})
|
||||
dispatch({ type: 'UPDATE_PERIOD', toPeriod, needConversion })
|
||||
},
|
||||
[dispatch, rules, situation]
|
||||
)
|
||||
const periods = ['mois', 'année']
|
||||
|
||||
return (
|
||||
|
|
|
@ -2,7 +2,7 @@ import withSitePaths from 'Components/utils/withSitePaths'
|
|||
import { encodeRuleName } from 'Engine/rules.js'
|
||||
import Fuse from 'fuse.js'
|
||||
import { compose, pick, sortBy } from 'ramda'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import React, { useMemo, useRef, useState } from 'react'
|
||||
import Highlighter from 'react-highlight-words'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, Redirect } from 'react-router-dom'
|
||||
|
@ -19,28 +19,23 @@ function SearchBar({
|
|||
const [inputValue, setInputValue] = useState(null)
|
||||
const [selectedOption, setSelectedOption] = useState(null)
|
||||
const inputElementRef = useRef()
|
||||
const fuse = useRef()
|
||||
// This operation is expensive, we don't want to do it everytime we re-render, so we cache its result
|
||||
const fuse = useMemo(() => {
|
||||
const list = rules.map(
|
||||
pick(['title', 'espace', 'description', 'name', 'dottedName'])
|
||||
)
|
||||
const options = {
|
||||
keys: [
|
||||
{ name: 'name', weight: 0.3 },
|
||||
{ name: 'title', weight: 0.3 },
|
||||
{ name: 'espace', weight: 0.2 },
|
||||
{ name: 'description', weight: 0.2 }
|
||||
]
|
||||
}
|
||||
return new Fuse(list, options)
|
||||
}, [rules])
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const options = {
|
||||
keys: [
|
||||
{ name: 'name', weight: 0.3 },
|
||||
{ name: 'title', weight: 0.3 },
|
||||
{ name: 'espace', weight: 0.2 },
|
||||
{ name: 'description', weight: 0.2 }
|
||||
]
|
||||
}
|
||||
if (!fuse.current) {
|
||||
// This operation is expensive, we don't want to do it everytime we re-render, so we cache its result in a reference
|
||||
fuse.current = new Fuse(
|
||||
rules.map(pick(['title', 'espace', 'description', 'name', 'dottedName'])),
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
const handleChange = selectedOption => {
|
||||
setSelectedOption(selectedOption)
|
||||
}
|
||||
const renderOption = ({ title, dottedName }) => (
|
||||
<span>
|
||||
<Highlighter searchWords={[inputValue]} textToHighlight={title} />
|
||||
|
@ -49,7 +44,7 @@ function SearchBar({
|
|||
</span>
|
||||
</span>
|
||||
)
|
||||
const filterOptions = (options, filter) => fuse.current.search(filter)
|
||||
const filterOptions = (options, filter) => fuse.search(filter)
|
||||
|
||||
if (selectedOption != null) {
|
||||
finallyCallback && finallyCallback()
|
||||
|
@ -68,8 +63,8 @@ function SearchBar({
|
|||
<>
|
||||
<Select
|
||||
value={selectedOption && selectedOption.dottedName}
|
||||
onChange={handleChange}
|
||||
onInputChange={inputValue => setInputValue(inputValue)}
|
||||
onChange={setSelectedOption}
|
||||
onInputChange={setInputValue}
|
||||
valueKey="dottedName"
|
||||
labelKey="title"
|
||||
options={rules}
|
||||
|
|
|
@ -14,6 +14,13 @@ export default compose(
|
|||
)(function SearchButton({ flatRules, invisibleButton }) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = e => {
|
||||
if (!(e.ctrlKey && e.key === 'k')) return
|
||||
setVisible(true)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
return () => {
|
||||
|
@ -21,14 +28,6 @@ export default compose(
|
|||
}
|
||||
}, [])
|
||||
|
||||
const handleKeyDown = e => {
|
||||
if (!(e.ctrlKey && e.key === 'k')) return
|
||||
setVisible(true)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
||||
|
||||
const close = () => setVisible(false)
|
||||
|
||||
return visible ? (
|
||||
|
|
|
@ -41,8 +41,14 @@ export default compose(
|
|||
const situation = useSituation()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const targets =
|
||||
analysis?.targets.filter(
|
||||
t =>
|
||||
!secondaryObjectives.includes(t.dottedName) &&
|
||||
t.dottedName !== 'contrat salarié . aides employeur'
|
||||
) || []
|
||||
|
||||
useEffect(() => {
|
||||
let targets = getTargets()
|
||||
// Initialize defaultValue for target that can't be computed
|
||||
targets
|
||||
.filter(
|
||||
|
@ -64,22 +70,10 @@ export default compose(
|
|||
)
|
||||
})
|
||||
|
||||
if (initialRender) {
|
||||
setInitialRender(false)
|
||||
}
|
||||
setInitialRender(false)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const getTargets = () => {
|
||||
if (!analysis) return []
|
||||
return analysis.targets.filter(
|
||||
t =>
|
||||
!secondaryObjectives.includes(t.dottedName) &&
|
||||
t.dottedName !== 'contrat salarié . aides employeur'
|
||||
)
|
||||
}
|
||||
|
||||
let targets = getTargets()
|
||||
|
||||
return (
|
||||
<div id="targetSelection">
|
||||
{(typeof objectifs[0] === 'string' ? [{ objectifs }] : objectifs).map(
|
||||
|
|
|
@ -38,10 +38,13 @@ export default compose(
|
|||
value: currentValue
|
||||
}) {
|
||||
const [touched, setTouched] = useState(false)
|
||||
const onChange = useCallback(value => {
|
||||
setFormValue(value)
|
||||
setTouched(true)
|
||||
})
|
||||
const onChange = useCallback(
|
||||
value => {
|
||||
setFormValue(value)
|
||||
setTouched(true)
|
||||
},
|
||||
[setFormValue]
|
||||
)
|
||||
|
||||
const renderBinaryQuestion = () => {
|
||||
return (
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
export default function SendButton({ disabled, submit }) {
|
||||
const getAction = useCallback(cause => (!disabled ? submit(cause) : null), [
|
||||
disabled,
|
||||
submit
|
||||
])
|
||||
useEffect(() => {
|
||||
const handleKeyDown = ({ key }) => {
|
||||
if (key !== 'Enter') return
|
||||
getAction('enter')
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [])
|
||||
}, [getAction])
|
||||
|
||||
const getAction = () => {
|
||||
return cause => (!disabled ? submit(cause) : null)
|
||||
}
|
||||
|
||||
const handleKeyDown = ({ key }) => {
|
||||
if (key !== 'Enter') return
|
||||
getAction()('enter')
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className="ui__ button plain"
|
||||
css="margin-left: 1.2rem"
|
||||
disabled={disabled}
|
||||
onClick={() => getAction()('accept')}>
|
||||
onClick={() => getAction('accept')}>
|
||||
<span className="text">
|
||||
<Trans>Suivant</Trans> →
|
||||
</span>
|
||||
|
|
|
@ -17,7 +17,7 @@ export default function AnimatedTargetValue({ value }: Props) {
|
|||
}
|
||||
setDifference((value || 0) - (previousValue || 0))
|
||||
setPreviousValue(value)
|
||||
}, [value])
|
||||
}, [previousValue, value])
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const format = value => {
|
||||
|
|
|
@ -131,7 +131,10 @@ export function appear({
|
|||
delay = 0,
|
||||
style
|
||||
}) {
|
||||
// TODO: We should rename this function Appear
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [show, setShow] = useState(unless)
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
window.setTimeout(() => setShow(true), 0)
|
||||
}, [])
|
||||
|
|
|
@ -79,7 +79,7 @@ export default function uniroot(
|
|||
|
||||
if (
|
||||
p < 0.75 * cb * q - Math.abs(tol_act * q) / 2 &&
|
||||
p < Math.abs(prev_step * q / 2)
|
||||
p < Math.abs((prev_step * q) / 2)
|
||||
) {
|
||||
// If (b + p / q) falls in [b,c] and isn't too large it is accepted
|
||||
new_step = p / q
|
||||
|
|
|
@ -58,7 +58,7 @@ const middlewares = [
|
|||
function InFranceRoute({ basename, language }) {
|
||||
useEffect(() => {
|
||||
setToSessionStorage('lang', language)
|
||||
}, [])
|
||||
}, [language])
|
||||
const paths = constructLocalizedSitePath(language)
|
||||
return (
|
||||
<Provider
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import classnames from 'classnames'
|
||||
import withTracker from 'Components/utils/withTracker'
|
||||
import { compose } from 'ramda'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { withRouter } from 'react-router'
|
||||
import backSvg from './back.svg'
|
||||
import mobileMenuSvg from './mobile-menu.svg'
|
||||
|
@ -19,10 +19,6 @@ type Props = OwnProps & {
|
|||
tracker: Tracker,
|
||||
location: Location
|
||||
}
|
||||
type State = {
|
||||
opened: boolean,
|
||||
sticky: boolean
|
||||
}
|
||||
|
||||
const bigScreen = window.matchMedia('(min-width: 1500px)')
|
||||
const isParent = (parentNode, children) => {
|
||||
|
@ -35,13 +31,19 @@ const isParent = (parentNode, children) => {
|
|||
return isParent(parentNode, children.parentNode)
|
||||
}
|
||||
|
||||
function SideBar({ location, tracker, children }) {
|
||||
function SideBar({ location, tracker, children }: Props) {
|
||||
const [opened, setOpened] = useState(false)
|
||||
const [sticky, setSticky] = useState(bigScreen.matches)
|
||||
const [previousLocation, setPreviousLocation] = useState(location)
|
||||
const ref = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = event => {
|
||||
if (!sticky && !isParent(ref.current, event.target) && opened) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('click', handleClick)
|
||||
bigScreen.addListener(handleMediaQueryChange)
|
||||
|
||||
|
@ -49,27 +51,22 @@ function SideBar({ location, tracker, children }) {
|
|||
window.removeEventListener('click', handleClick)
|
||||
bigScreen.removeListener(handleMediaQueryChange)
|
||||
}
|
||||
}, [opened, sticky])
|
||||
}, [handleClose, opened, sticky])
|
||||
|
||||
useEffect(() => {
|
||||
if (!sticky && previousLocation !== location) {
|
||||
setOpened(false)
|
||||
}
|
||||
setPreviousLocation(location)
|
||||
})
|
||||
}, [sticky, previousLocation, location])
|
||||
|
||||
const handleClick = event => {
|
||||
if (!sticky && !isParent(ref.current, event.target) && opened) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
const handleMediaQueryChange = () => {
|
||||
setSticky(bigScreen.matches)
|
||||
}
|
||||
const handleClose = () => {
|
||||
const handleClose = useCallback(() => {
|
||||
tracker.push(['trackEvent', 'Sidebar', 'close'])
|
||||
setOpened(false)
|
||||
}
|
||||
}, [tracker])
|
||||
const handleOpen = () => {
|
||||
tracker.push(['trackEvent', 'Sidebar', 'open'])
|
||||
setOpened(true)
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function IntegrationTest() {
|
|||
script.dataset.couleur = colour
|
||||
domNode.current.innerHTML = ''
|
||||
domNode.current.appendChild(script)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [version])
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import LangSwitcher from 'Components/LangSwitcher'
|
||||
import marianneSvg from 'Images/marianne.svg'
|
||||
import urssafSvg from 'Images/urssaf.svg'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
@ -9,8 +9,10 @@ import screenfull from 'screenfull'
|
|||
import { isIE } from '../../../../utils'
|
||||
|
||||
export default function IframeFooter() {
|
||||
const [isFullscreen, setIsFullscreen] = useState(screenfull.isFullscreen)
|
||||
useEffect(() => {
|
||||
screenfull.enabled && screenfull.onchange(() => this.forceUpdate())
|
||||
screenfull.enabled &&
|
||||
screenfull.onchange(() => setIsFullscreen(screenfull.isFullscreen))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
@ -44,7 +46,7 @@ export default function IframeFooter() {
|
|||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<LangSwitcher className="ui__ button simple" />
|
||||
{screenfull.enabled && !screenfull.isFullscreen && !isIE() && (
|
||||
{screenfull.enabled && !isFullscreen && !isIE() && (
|
||||
<button
|
||||
className="ui__ button small"
|
||||
onClick={() => {
|
||||
|
|
|
@ -3553,6 +3553,11 @@ eslint-plugin-flowtype@^3.2.1:
|
|||
dependencies:
|
||||
lodash "^4.17.15"
|
||||
|
||||
eslint-plugin-react-hooks@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.0.1.tgz#e898ec26a0a335af6f7b0ad1f0bedda7143ed756"
|
||||
integrity sha512-xir+3KHKo86AasxlCV8AHRtIZPHljqCRRUYgASkbatmt0fad4+5GgC7zkT7o/06hdKM6MIwp8giHVXqBPaarHQ==
|
||||
|
||||
eslint-plugin-react@^7.12.4:
|
||||
version "7.14.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz#911030dd7e98ba49e1b2208599571846a66bdf13"
|
||||
|
|
Loading…
Reference in New Issue