I'm working on a Notification feature in my app (pretty much like Facebook notifications).
When I click a button in the header navigation, the dropdown opens and shows the notification list. The notification has a Link (from react-router) in it.
What I need to do is to close the dropdown whenever a Link is clicked.
Here's roughly the hierarchy I currently have:
Header > Navigation > Button > Dropdown > List > Notification > Link
Since the dropdown functionality is used more that once, I've abstracted its behavior away into a HOC that uses render prop:
export default function withDropDown(ClickableElement) {
return class ClickableDropdown extends PureComponent {
static propTypes = {
children: PropTypes.func.isRequired,
showOnInit: PropTypes.bool,
};
static defaultProps = {
showOnInit: false,
};
state = {
show: !!this.props.showOnInit,
};
domRef = createRef();
componentDidMount() {
document.addEventListener('mousedown', this.handleGlobalClick);
}
toggle = show => {
this.setState({ show });
};
handleClick = () => this.toggle(true);
handleGlobalClick = event => {
if (this.domRef.current && !this.domRef.current.contains(event.target)) {
this.toggle(false);
}
};
render() {
const { children, ...props } = this.props;
return (
<Fragment>
<ClickableElement {...props} onClick={this.handleClick} />
{this.state.show && children(this.domRef)}
</Fragment>
);
}
};
}
The HOC above encloses the Button component, so I have:
const ButtonWithDropdown = withDropdown(Button);
class NotificationsHeaderDropdown extends PureComponent {
static propTypes = {
data: PropTypes.arrayOf(notification),
load: PropTypes.func,
};
static defaultProps = {
data: [],
load: () => {},
};
componentDidMount() {
this.props.load();
}
renderDropdown = ref => (
<Dropdown ref={ref}>
{data.length > 0 && <List items={this.props.data} />}
{data.length === 0 && <EmptyList />}
</Dropdown>
);
render() {
return (
<ButtonWithDropdown count={this.props.data.length}>
{this.renderDropdown}
</ButtonWithDropdown>
);
}
}
List and Notification are both dumb functional components, so I'm not posting their code here. Dropdown is pretty much the same, with the difference it uses ref forwarding.
What I really need is to call that .toggle() method from ClickableDropdown created by the HOC to be called whenever I click on a Link on the list.
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
I'm using redux, but I'm not sure this is the kind of thing I'd put on the store.
Or should I handle this imperatively using the DOM API, by changing the implementation of handleGlobalClick from ClickableDropdown?
Edit:
I'm trying with the imperative approach, so I've changed the handleGlobalClick method:
const DISMISS_KEY = 'dropdown';
function contains(current, element) {
if (!current) {
return false;
}
return current.contains(element);
}
function isDismisser(dismissKey, current, element) {
if (!element || !contains(current, element)) {
return false;
}
const shouldDismiss = element.dataset.dismiss === dismissKey;
return shouldDismiss || isDismisser(dismissKey, current, element.parentNode);
}
// Then...
handleGlobalClick = event => {
const containsEventTarget = contains(this.domRef.current, event.target);
const shouldDismiss = isDismisser(
DISMISS_KEY,
this.domRef.current,
event.target
);
if (!containsEventTarget || shouldDismiss) {
this.toggle(false);
}
return true;
};
Then I changed the Link to include a data-dismiss property:
<Link
to={url}
data-dismiss="dropdown"
>
...
</Link>
Now the dropdown is closed, but I'm not redirected to the provided url anymore.
I tried to defer the execution of this.toggle(false) using requestAnimationFrame and setTimeout, but it didn't work either.
Solution:
Based on the answer by #streletss bellow, I came up with the following solution:
In order to be as generic as possible, I created a shouldHideOnUpdate prop in the ClickableDropdown dropdown component, whose Hindley-Milner-ish signature is:
shouldHideOnUpdate :: Props curr, Props prev => (curr, prev) -> Boolean
Here's the componentDidUpdate implementation:
componentDidUpdate(prevProps) {
if (this.props.shouldHideOnUpdate(this.props, prevProps)) {
this.toggle(false);
}
}
This way, I didn't need to use the withRouter HOC directly in my withDropdown HOC.
So, I lifted the responsibility of defining the condition for hiding the dropdown to the caller, which is my case is the Navigation component, where I did something like this:
const container = compose(withRouter, withDropdown);
const ButtonWithDropdown = container(Button);
function routeStateHasChanged(currentProps, prevProps) {
return currentProps.location.state !== prevProps.location.state;
}
// ... then
render() {
<ButtonWithDropdown shouldHideOnUpdate={routeStateHasChanged}>
{this.renderDropdown}
</ButtonWithDropdown>
}
It seems you could simply make use of withRouter HOC and check if this.props.location.pathname has changed when componentDidUpdate:
export default function withDropDown(ClickableElement) {
class ClickableDropdown extends Component {
// ...
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.toggle(false);
}
}
// ...
};
return withRouter(ClickableDropdown)
}
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
In the question, you mention that you are using redux.So I assume that you store showOnInit in redux.We don't usually store a function in redux.In toggle function,I think you should dispatch an CHANGE_SHOW action to change the showOnInit in redux, then pass the show data not the function to the children component.Then after reducer dispatch,the react will change “show” automatically.
switch (action.type) {
case CHANGE_SHOW:
return Object.assign({}, state, {
showOnInit: action.text
})
...
default:
return state
}
Link element and data pass
Use the property in Link-to,not data-...Like this:
<Link
to={{
pathname: url,
state:{dismiss:"dropdown"}
}}
/>
And the state property will be found in this.props.location.
give context a little try(not recommend)
It may lead your project to instable and some other problems.(https://reactjs.org/docs/context.html#classcontexttype)
First,define context
const MyContext = React.createContext(defaultValue);
Second,define pass value
<MyContext.Provider value={this.toggle}>
Then,get the value in the nested component
<div value={this.context} />
Related
I wrote a class component and mutilple functions in it(class) , but don't know how variable be passing between different function.
class App extends Component {
state = {
stringA:null,
stringB:null
};
set_A = (event) =>{
const stringA = 'text';
}
copy_A = (event) =>{
const stringB = stringA;
}
render() {
return (
<>
<button onClick={this.set_A} type="click">set</button>
<button onClick={this.copy_A} type="click">copy</button>
</>
);}
}
export default App;
I reference this docs , but it only said function component without class component.
https://www.robinwieruch.de/react-function-component
or, are state and props not a kind of variable?
You access your properties with this.props and your state with this.state. You change state by calling setState which accepts partial states and merges them into the full state. It also triggers a re-render so that state changes can be seen in the UI.
class App extends Component {
state = {
stringA:null,
stringB:null
};
set_A = (event) => {
this.setState({ stringA: 'text' });
}
copy_A = (event) => {
this.setState({ stringB: this.state.stringA });
}
render() {
return (
<>
<button onClick={this.set_A} type="click">set</button>
<button onClick={this.copy_A} type="click">copy</button>
</>
);
}
}
export default App;
So in React, you would not be assigning a value to a variable like that. You would be utilizing State functionality. For Class-based React you would be using this.setState({stringA: 'text'})
or
this.setState({stringB: stringA})
Once the values are in the state you can access them anywhere in the component from the state object this.state.stringB for instance would have the value that was set once you had clicked on copy button
Example
set_A = (event) => {
this.setState({ stringA: 'text' });
console.log(this.state.stringB)
}
copy_A = (event) => {
this.setState({ stringB: this.state.stringA });
}
React Documentation is also a great resource to reference for Class and Function based component behaviors. https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class
You change state by calling setState in class based components
Try this :
class App extends Component {
state = { stringA:null, stringB:null };
set_A = (e) => {
this.setState({...state, stringA: 'text' });
}
copy_A = (e) => {
this.setState({ ...state,stringB: this.state.stringA });
}
render() {
return (
<>
<button onClick={this.set_A} type="click">set</button>
<button onClick={this.copy_A} type="click">copy</button>
</>
);
}
}
export default App;
Use the spread operator {...state} to change only the targeted piece of state you want to change with no change in the other pieces of the state.
Let us assume we have a statefull React component (configured to work with Redux):
export class SomeComponent extends Component {
state = {
someObject: {}
};
componentWillMount() {
this.props.getNews();
this.props.getFakeNews();
}
render() {
const {
news,
fakeNews
} = this.props;
if(_.isEmpty(news) || _.isEmpty(fakeNews)){
return <div>Loading</div>
}else{
return <div>Here all component stuff</div>
}
}
SomeComponent.propTypes = {
news: PropTypes.array.isRequired,
fakeNews: PropTypes.array.isRequired
};
export const Some = connect(
state => ({
news: newsSelectors.list(state),
fakeNews: fakeNewsSelectors.list(state)
}),
{
getNews,
getFakeNEws
}
)(withStyles(styles)(SomeComponent), withRouter(SomeComponent));
This component will re-render two times during getting news and fake news. In the render method we need to check if both of them are loaded.
Is there any way to trigger render only when all props are loaded?
In a perfect scenario I'd like to have no detailed null/empty check on the set of props. I believe React or Redux should perform this operation on its own as long the prop is configured as required.
You can add a lifecycle method `shouldComponentUpdate(nextProps, nextState).
You can add the following method and it should resolve it for you:
shouldComponentUpdate(nextProps, nextState) {
if (_.isEmpty(nextProps.news) || _.isEmpty(nextProps.fakeNews)) {
return false;
}
return true;
}
You could do something like:
// HOC factory
function ifComponent (predicate, PlaceHolder) {
return Component => class If extends React.Component {
render () {
if (predicate(this.props)) {
return <Component {...this.props} />
}
return <PlaceHolder {...this.props} />
}
}
}
}
// create the customHOC
const whenPropsLoaded = ifComponent(props => props.news && props.fakeNews, Loader);
// compose the two HOCs using the `compose` function in redux (simple function composition)
const News = compose(
connect(getNewsProps),
whenPropsLoaded(DisplayNews)
);
As a side note you may be interested in the recompose utility library bad its branch HOC (docs here). I think this is pretty much what you want as you seem to know about HOCs.
If you want to avoid null and undefined values from redux. You can use Selectors it was very easy to avoid those things.
const newsSelectors = (state) => {
if(!state.list) { *//list is null or undefined*
return [] or {} *//your wish (Proptypes required value)*
}
else {
return state.list
}
}
export { newsSelectors };
I think you can solve the issue if you rewrite the render function as below.
render() {
const {
news,
fakeNews
} = this.props;
return (
{news && fakeNews ?
<div>Here all component stuff</div>
: <div>Loading</div> }
)
}
I hope this helps you.
I have a parent component which maintains state for three 'form' components that render in sequence. It looks something like this:
<Parent>
{ renderFormBasedOnState() }
</Parent>
FormA renders, then when next is click FormB renders then FormC renders, all in the parent.
Previously I was using a React Router to do this, but the problem is, I don't want the user to be able to bookmark /formb or /formc, as that would be an invalid state.
I can do this with a switch statement, but then I lose forward / back button browser history ability - and I don't want to basically re-implement react-router in my component. What is the simplest way to go about this?
Haven't tried it for the back of the browser, but it could look something like this:
export default class tmp extends React.Component {
state = {
currentVisibleForm: 'A'
}
onBackButtonEvent = (e) => {
if(this.state.currentVisibleForm !== 'A') {
e.preventDefault();
//Go back to the previous visible form by changing the state
} else {
// Nothing to do
}
}
componentDidMount = () => {
window.onpopstate = this.onBackButtonEvent;
}
render() {
return (
<Parent>
{this.state.currentVisibleForm === 'A' &&
<FormA />
}
{this.state.currentVisibleForm === 'B' &&
<FormB />
}
{this.state.currentVisibleForm === 'C' &&
<FormC />
}
</Parent>
)
}
}
Tell me if it is of any help!
So I was able to get this working with the history api, however it may not be worth the effort to fine tune - I may revert. Managing state in two places is kind of dumb. Note this history object is the same from the application's 'Router' component, and doesn't conflict.
state = {
FormData: {},
action: 'Form_1'
}
componentWillMount() {
this.unlistenHistory = history.listen((location) => {
if (location.state) {
this.setState(() => ({
action: location.state.action
}));
}
});
history.push(undefined, {action: 'FORM_1'});
}
componentWillUnmount() {
this.unlistenHistory();
}
finishForm1 = () => {
const action = 'Form_2';
history.push(undefined, { action });
this.setState((prevState) => ({
// business stuff,
action
}));
};
renderCurrentState() {
switch(this.state.action) {
case 'FORM_1':
return <Form1 />
...
}
}
render() {
return (
<div>
{ this.renderCurrentState() }
</div>
);
}
I have two DropTargets, ComponentA and ComponentB arranged in this structure:
<ComponentA
dropEvent={this.handleRootDropEvent}
>
<p>Displayed Categories </p>
{
this.state.categories.map((c, idx) =>
<ComponentB
key={idx}
parentCat={null}
thisCat={c}
level={c.level}
dropEvent={this.handleDropEvent}
/>
)
}
</ComponentA>
And the problem I'm having in particular is with the drop target spec drop function. Both of the DropTargets (for ComponentA and B) have the same drop function defined at the moment:
//ComponentB
const targetSpec = {
drop(props, monitor, component) {
const item = monitor.getItem()
if (props.thisCat.category_id == item.category_id) return
component.setState({ droppedItem: item })
component.setState({ droppedItem: null })
},
canDrop(props, monitor) {
const item = monitor.getItem()
if (props.thisCat.category_id == item.category_id) return false
return true
},
hover(props, monitor, component){
}
}
// Component A
const source = {
drop(props, monitor, component) {
const item = monitor.getItem()
component.setState({ droppedItem: item })
component.setState({ droppedItem: null })
}
}
(I can confirm that both are getting called on drop events, and that item is not null in ComponentA's drop function)
It seems that component.setState({}) actually changes the props of the component it's called on, or at least that's how I'm able to use it in the case of ComponentB:
componentWillReceiveProps = () => {
if (this.props.droppedItem) {
this.props.dropEvent(this.props.droppedItem, this.props.thisCat)
}
}
And that code works, I'm able to trigger a function in the parent component (or this case grandparent) of ComponentB by checking for the presence of those props that are set in the drop function.
However, in the same exact componentWillReceiveProps function definition in ComponentA, this.props.droppedItem is always undefined.
Any idea what I could try to get the props passed successfully? Am I misunderstanding React DnD's component.setState({}) API?
I have a dropdown as is shown in the following image:
When I click the folder icon it opens and closes because showingProjectSelector property in the state that is set to false.
constructor (props) {
super(props)
const { organization, owner, ownerAvatar } = props
this.state = {
owner,
ownerAvatar,
showingProjectSelector: false
}
}
When I click the icon, it opens and closes properly.
<i
onClick={() => this.setState({ showingProjectSelector: !this.state.showingProjectSelector })}
className='fa fa-folder-open'>
</i>
But what I'm trying to do is to close the dropdown when I click outside it. How can I do this without using any library?
This is the entire component: https://jsbin.com/cunakejufa/edit?js,output
You could try leveraging onBlur:
<i onClick={...} onBlur={() => this.setState({showingProjectSelector: false})}/>
I faced same issue with you. Solved after reading this:
Detect click outside React component
Please try:
You should use a High Order Component to wrap the component that you would like to listen for clicks outside it.
This component example has only one prop: "onClickedOutside" that receives a function.
ClickedOutside.js
import React, { Component } from "react";
export default class ClickedOutside extends Component {
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
handleClickOutside = event => {
// IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked component
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
// A props callback for the ClikedClickedOutside
this.props.onClickedOutside();
}
};
render() {
// In this piece of code I'm trying to get to the first not functional component
// Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)
let firstNotFunctionalComponent = this.props.children;
while (typeof firstNotFunctionalComponent.type === "function") {
firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;
}
// Here I'm cloning the element because I have to pass a new prop, the "reference"
const children = React.cloneElement(firstNotFunctionalComponent, {
ref: node => {
this.wrapperRef = node;
},
// Keeping all the old props with the new element
...firstNotFunctionalComponent.props
});
return <React.Fragment>{children}</React.Fragment>;
}
}
If you want to use a tiny component (466 Byte gzipped) that already exists for this functionality then you can check out this library react-outclick.
The good thing about the library is that it also lets you detect clicks outside of a component and inside of another. It also supports detecting other types of events.
Using the library you can have something like this inside your component.
import OnOutsiceClick from 'react-outclick';
class MyComp extends Component {
render() {
return (
<OnOutsiceClick
onOutsideClick={() => this.setState({showingProjectSelector: false})}>
<Dropdown />
</OnOutsiceClick>
);
}
}
Wrapper component - i.e. the one that wrapps all other components
create onClick event that runs a function handleClick.
handleClick function checks ID of the clicked event.
When ID matches it does something, otherwise it does something else.
const handleClick = (e) => {
if(e.target.id === 'selectTypeDropDown'){
setShowDropDown(true)
} else {
setShowDropDown(false);
}
}
So I have a dropdown menu that appears ONLY when you click on the dropdown menu, otherwise it hides it.