diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index f4d9c68bd..b5c000473 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -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
diff --git a/package.json b/package.json
index 4bbf6b38b..26e75d1b2 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/source/components/AttachDictionary.js b/source/components/AttachDictionary.js
index 42fbba6f8..733132fcf 100644
--- a/source/components/AttachDictionary.js
+++ b/source/components/AttachDictionary.js
@@ -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
diff --git a/source/components/CurrencyInput/CurrencyInput.js b/source/components/CurrencyInput/CurrencyInput.js
index 2dcd46ccc..7507264ff 100644
--- a/source/components/CurrencyInput/CurrencyInput.js
+++ b/source/components/CurrencyInput/CurrencyInput.js
@@ -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 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 (
5
- ? { style: { width: `${5 + (valueLength - 5) * 0.75}em` } }
- : {})}>
+ {...(valueLength > 5 ? { style: { width } } : {})}>
{isCurrencyPrefixed && '€'}
{
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 && <> €>}
diff --git a/source/components/PercentageField.js b/source/components/PercentageField.js
index e0abf0e7d..e85eabf7a 100644
--- a/source/components/PercentageField.js
+++ b/source/components/PercentageField.js
@@ -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 (
diff --git a/source/components/PeriodSwitch.js b/source/components/PeriodSwitch.js
index e87d33920..1feb823a1 100644
--- a/source/components/PeriodSwitch.js
+++ b/source/components/PeriodSwitch.js
@@ -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 (
diff --git a/source/components/SearchBar.js b/source/components/SearchBar.js
index 31f384035..5294b3919 100644
--- a/source/components/SearchBar.js
+++ b/source/components/SearchBar.js
@@ -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 }) => (
@@ -49,7 +44,7 @@ function SearchBar({
)
- 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({
<>