schiehll / react-alert

alerts for React
MIT License
608 stars 98 forks source link

How to Call the "show"/"success"/"alert" outside of a component ? #92

Closed bastoune closed 5 years ago

bastoune commented 6 years ago

I would like to call the opening of an alert outside of a component (for example because the component will be unmounted or so)

But I can't find how to import the right functions ?

jacobsowles commented 5 years ago

Do you have some code that demonstrates what you're trying to do? The mechanism behind react-alert is that it passes an alert object from the Provider to a component via a prop, so the alert can only be used inside a component.

geocine commented 5 years ago

One use case of this is to call it outside in a helper module. Let's say I have an http.js helper module which I call fetch and also check 400/500 statuses. When I use this library for http calls , I can always use it to display this alert whenever I encounter an error.

besLisbeth commented 5 years ago

@geocine, do you use any state management library? From my point of view, you need to have a mediator class, if to take Redux or Mobx - it will be Store. It can control the http requests, catch errors and contain an observable value on which the AlertComponent will react as soon as value changes. What do you think?

geocine commented 5 years ago

@besLisbeth unfortunately the code I am maintaining doesn't have a Store, but regarding that point I am looking for a way to actually trigger the alert from the mediator class, not on the component level.

besLisbeth commented 5 years ago

@geocine, then I think the only way is to store the reference to the alert provider in your mediator class like that

//Component.jsx
this.alert = React.createRef(); 
render() {
    return <AlertProvider ref={this.alert} />;
}

//MediatorClass.js
setAlertRef(ref){
   this.alertRef = ref;
}

showSuccessAlert(){
   this.alertRef.success(); //or show(), or error(), so on
}

Hope, it will help

geocine commented 5 years ago

@besLisbeth I will try it out as soon as I can thanks again.

geocine commented 5 years ago

@besLisbeth , I was able to let the alert show using your suggestion, by doing this

helpers/alert.js - singleton AlertRef


import React from 'react';
export class AlertRef {

  static instance = null;
  static createInstance() {
      var object = React.createRef();
      return object;
  }

  static getInstance () {
      if (!AlertRef.instance) {
          AlertRef.instance = AlertRef.createInstance();
      }
      return AlertRef.instance;
  }
}

index.js

import { AlertRef } from './helpers/alert'; // reference to singleton AlertRef
class Root extends Component  { 
  render () {
    return (
      <AlertProvider template={AlertTemplate} {ref={AlertRef.getInstance()}>
        <App />
      </AlertProvider>
    )
  }
}

http.js - mediator

import { AlertRef } from './helpers/alert'; // reference to singleton AlertRef
export const getHttp = (path) => {
  return fetch(`${path}`, {
    headers: getHeaders()
  }).then(response => checkStatus(response))
  .catch(error => AlertRef.getInstance().current.show(error.message))
}

This is working however I have the alert showing endlessly Imgur

Any thoughts how to do this correctly?

besLisbeth commented 5 years ago

Hi, @geocine, it looks like the problem is not in the code which you attached. An alert showing is the result of an error happened in a network request and the error from it showed in console endlessly. I think, that an error somewhere in the calling the network request method, can you check it by yourself once more?

besLisbeth commented 5 years ago

@geocine, also, do you really need the singleton class AlertRef? Maybe it can be simplified into just storing and updating (if needed) refs?

geocine commented 5 years ago

@besLisbeth , I am purposely throwing a 404, so that the alert will show. I will use it specifically for this use case. If I do not add the AlertRef , the 404 requests only fires once and an alert will not show.

besLisbeth commented 5 years ago

@geocine, I just ran your code from an example and it worked for me. Can you please deploy the code with an endless loop somewhere in the sandbox? It still looks to me like the problem not in the AlertRef itself.

geocine commented 5 years ago

@besLisbeth you can try my repo . https://github.com/geocine/react-auth-demo

besLisbeth commented 5 years ago

Continued in geocine/react-auth-demo#1

schiehll commented 5 years ago

Closing this as @besLisbeth helped already. Thank you, BTW!

RyanBertrand commented 5 years ago

@geocine's example no longer works in v5 because the Provider is no longer a class which prevents React from using Refs. Downgrading to 4.0.4 works.

besLisbeth commented 5 years ago

Hi, @RyanBertrand! Can you please explain a little bit how did you attempt to use a reference to the alert to find the solution to the problem? Do you use mobx, redux or smth else?

I think that in most cases using a reference from useAlert() hook will work.

RyanBertrand commented 5 years ago

@besLisbeth I needed to show an alert outside of a component. The new hooks functionality is great but it does not work in class components which is what all of our components are.

From (Hooks FAQ)

You can’t use Hooks inside of a class component

Here is how I use refs to show alerts anywhere in the app (not just components).

Alerts.js

import React from 'react';

export default class Alerts {
    static _reactAlertRef = React.createRef();

    static getRef(){
        return this._reactAlertRef;
    }

    static show(type, message){
        let reactAlert = Alerts.getRef().current;
        let showFn = reactAlert[type];

        showFn(message);
    }

    static showInfo(message){
        this.show('info', message);
    }

    static showSuccess(message){
        this.show('success', message);
    }

    static showError(message){
        this.show('error', message);
    }
}

AppWrapper.js

import Alerts from './Alerts';

class AppWrapper extends Component {
    render() {
        let alertRef = Alerts.getRef();

        return (
            <AlertProvider ref={alertRef}
                           template={AlertTemplate}>
                <App/>
            </AlertProvider>
        );
    }
}

export default AppWrapper;

Now from anywhere in the app, we can show an alert by calling:

Alerts.showError("Oops!");

This allows us to show alerts from our Redux (thunk) actions without components needing to know what is going on.

besLisbeth commented 5 years ago

@RyanBertrand, thank you a lot for the detailed explanation. So, yes, it's clear that Hooks cannot be used outside of a class component. But the value which you get from the hook useAlert or HOC withAlert can be used as in your example.

I think that we have a misunderstanding here:

Here is how I use refs to show alerts anywhere in the app (not just components).

Anyway, because alerts themselves are components, they should be managed at the component level. Another question is how you can archive this.

My proposal of how you can use version 5 of the library:

  1. You can get the reference from useAlert hook or withAlert HOC in and pass it to your Alerts.js. But in that case, methods of the class will stop to be static, so you will need to create an instance of the class, pass the reference to alert and keep it somewhere to be available from any part of the project. That case seems to me unintuitive because it mixing the components logic with business logic of the project.

  2. Structure <App/> component this way:

    <Alerts>
    <EverythingElse/>
    </Alerts>

And use <Alerts/> component as Consumer for AlertProvider - get the reference to alerts from useAlert hook or withAlert HOC and wait for dispatching the actions. Basing on the code above, I propose, that you will have such actions as SHOW_SUCCESS/INFO/ERROR. Then, when something will hapen in your aplication, you will dispatch the needed action, and component <Alerts/> will wait for them.

What do you think, the second option will meet your needs, or it is totally unacceptable?

kauffmanes commented 5 years ago

@RyanBertrand thank you! Your answer worked great for me. My React app is running on a robot so it's always getting alerts about getting stuck or having some type of hardware problem. I ran into an issue where when the robot is stuck, instead of saying "I'm stuck once", it says it constantly until the issue is fixed (pub/sub with lots of pubs). I modified your answer to include this:

static show(type, message) {
  const reactAlert = Alerts.getRef().current;
  const showFn = reactAlert[type];

  // if new message is already in the current list of alerts
  if (message && reactAlert.state.alerts && !(reactAlert.state.alerts.some((alert) => (
    alert.message === message
  )))) {
    showFn((typeof message === 'string' || typeof message === 'number') ? message : JSON.stringify(message));
    }
}

^^ In case anyone else runs into this.

oakland commented 3 years ago

@RyanBertrand This doesn't work any more? I got Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?.

I'm using latest version of react-alert, which is v7.0.2.

Does your solution still work if you update react-alert?

After looking into the source code of react-alert, I think maybe we cannot passing ref anymore.