-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(customPropTypes): improve perf of suggest
#2314
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,63 +12,80 @@ export const as = (...args) => PropTypes.oneOfType([ | |
PropTypes.func, | ||
])(...args) | ||
|
||
/* eslint-disable max-nested-callbacks */ | ||
const findBestSuggestions = _.memoize((propValueWords, suggestions) => _.flow( | ||
_.map((suggestion) => { | ||
const suggestionWords = suggestion.split(' ') | ||
|
||
const propValueScore = _.flow( | ||
_.map(x => _.map(y => leven(x, y), suggestionWords)), | ||
_.map(_.min), | ||
_.sum, | ||
)(propValueWords) | ||
|
||
const suggestionScore = _.flow( | ||
_.map(x => _.map(y => leven(x, y), propValueWords)), | ||
_.map(_.min), | ||
_.sum, | ||
)(suggestionWords) | ||
|
||
return { suggestion, score: propValueScore + suggestionScore } | ||
}), | ||
_.sortBy(['score', 'suggestion']), | ||
_.take(3), | ||
)(suggestions)) | ||
/* eslint-enable max-nested-callbacks */ | ||
|
||
/** | ||
* Similar to PropTypes.oneOf but shows closest matches. | ||
* Word order is ignored allowing `left chevron` to match `chevron left`. | ||
* Useful for very large lists of options (e.g. Icon name, Flag name, etc.) | ||
* @param {string[]} suggestions An array of allowed values. | ||
*/ | ||
export const suggest = suggestions => (props, propName, componentName) => { | ||
export const suggest = (suggestions) => { | ||
if (!Array.isArray(suggestions)) { | ||
throw new Error([ | ||
'Invalid argument supplied to suggest, expected an instance of array.', | ||
` See \`${propName}\` prop in \`${componentName}\`.`, | ||
].join('')) | ||
throw new Error('Invalid argument supplied to suggest, expected an instance of array.') | ||
} | ||
const propValue = props[propName] | ||
|
||
// skip if prop is undefined or is included in the suggestions | ||
if (_.isNil(propValue) || propValue === false || _.includes(propValue, suggestions)) return | ||
/* eslint-disable max-nested-callbacks */ | ||
const findBestSuggestions = _.memoize((str) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implementation is the same, just passed the base string in instead of an array so that let calls = 0
const fn = _.memoize((arr) => ++calls)
fn(['my', 'words'])
fn(['my', 'words'])
console.log(calls) // 2 Note that the previous usage of this memoized function was incorrect, since it would ignore the second argument The real fix would be to write our own resolver function for the memoization, but it would essentially achieve the same thing as I'm doing here by inlining the function, given the use case for this |
||
const propValueWords = str.split(' ') | ||
|
||
return _.flow( | ||
_.map((suggestion) => { | ||
const suggestionWords = suggestion.split(' ') | ||
|
||
const propValueScore = _.flow( | ||
_.map(x => _.map(y => leven(x, y), suggestionWords)), | ||
_.map(_.min), | ||
_.sum, | ||
)(propValueWords) | ||
|
||
const suggestionScore = _.flow( | ||
_.map(x => _.map(y => leven(x, y), propValueWords)), | ||
_.map(_.min), | ||
_.sum, | ||
)(suggestionWords) | ||
|
||
return { suggestion, score: propValueScore + suggestionScore } | ||
}), | ||
_.sortBy(['score', 'suggestion']), | ||
_.take(3), | ||
)(suggestions) | ||
}) | ||
/* eslint-enable max-nested-callbacks */ | ||
|
||
// Convert the suggestions list into a hash map for O(n) lookup times. Ensure | ||
// the words in each key are sorted alphabetically so that we have a consistent | ||
// way of looking up a value in the map, i.e. we can sort the words in the | ||
// incoming propValue and look that up without having to check all permutations. | ||
const suggestionsLookup = suggestions.reduce((acc, key) => { | ||
acc[key.split(' ').sort().join(' ')] = true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Key sorting |
||
return acc | ||
}, {}) | ||
|
||
return (props, propName, componentName) => { | ||
const propValue = props[propName] | ||
|
||
// skip if prop is undefined or is included in the suggestions | ||
if (!propValue || suggestionsLookup[propValue]) return | ||
|
||
// check if the words were correct but ordered differently. | ||
// Since we're matching for classNames we need to allow any word order | ||
// to pass validation, e.g. `left chevron` vs `chevron left`. | ||
const propValueSorted = propValue.split(' ').sort().join(' ') | ||
if (suggestionsLookup[propValueSorted]) return | ||
|
||
// find best suggestions | ||
const propValueWords = propValue.split(' ') | ||
const bestMatches = findBestSuggestions(propValueWords, suggestions) | ||
// find best suggestions | ||
const bestMatches = findBestSuggestions(propValue) | ||
|
||
// skip if a match scored 0 | ||
// since we're matching on words (classNames) this allows any word order to pass validation | ||
// e.g. `left chevron` vs `chevron left` | ||
if (bestMatches.some(x => x.score === 0)) return | ||
// skip if a match scored 0 | ||
if (bestMatches.some(x => x.score === 0)) return | ||
|
||
return new Error([ | ||
`Invalid prop \`${propName}\` of value \`${propValue}\` supplied to \`${componentName}\`.`, | ||
`\n\nInstead of \`${propValue}\`, did you mean:`, | ||
bestMatches.map(x => `\n - ${x.suggestion}`).join(''), | ||
'\n', | ||
].join('')) | ||
return new Error([ | ||
`Invalid prop \`${propName}\` of value \`${propValue}\` supplied to \`${componentName}\`.`, | ||
`\n\nInstead of \`${propValue}\`, did you mean:`, | ||
bestMatches.map(x => `\n - ${x.suggestion}`).join(''), | ||
'\n', | ||
].join('')) | ||
} | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to perform this
isArray
check every time the component is checked, since it's only used to initially define prop types. Given my current change we lose some information about which property was specified incorrectly, but the benefit is that if it is used improperly it will throw immediately.