Multiple event listeners in Next.js router












1















Currently next/router exposes a singleton API where listening to its changes can be done via:



Router.onRouteChangeStart = myHandler // subscribe

Router.onRouteChangeStart = null // unsubscribe


This poses several architecture-related challenges as two unrelated components can't listen to route state changes at the same time.



Based on the discussion on https://github.com/zeit/next.js/issues/2033 there is no plan to convert next/router to an Event Emitter / Observable.



Given that, how can we implement a router with shared subscriptions in Next.js?










share|improve this question



























    1















    Currently next/router exposes a singleton API where listening to its changes can be done via:



    Router.onRouteChangeStart = myHandler // subscribe

    Router.onRouteChangeStart = null // unsubscribe


    This poses several architecture-related challenges as two unrelated components can't listen to route state changes at the same time.



    Based on the discussion on https://github.com/zeit/next.js/issues/2033 there is no plan to convert next/router to an Event Emitter / Observable.



    Given that, how can we implement a router with shared subscriptions in Next.js?










    share|improve this question

























      1












      1








      1








      Currently next/router exposes a singleton API where listening to its changes can be done via:



      Router.onRouteChangeStart = myHandler // subscribe

      Router.onRouteChangeStart = null // unsubscribe


      This poses several architecture-related challenges as two unrelated components can't listen to route state changes at the same time.



      Based on the discussion on https://github.com/zeit/next.js/issues/2033 there is no plan to convert next/router to an Event Emitter / Observable.



      Given that, how can we implement a router with shared subscriptions in Next.js?










      share|improve this question














      Currently next/router exposes a singleton API where listening to its changes can be done via:



      Router.onRouteChangeStart = myHandler // subscribe

      Router.onRouteChangeStart = null // unsubscribe


      This poses several architecture-related challenges as two unrelated components can't listen to route state changes at the same time.



      Based on the discussion on https://github.com/zeit/next.js/issues/2033 there is no plan to convert next/router to an Event Emitter / Observable.



      Given that, how can we implement a router with shared subscriptions in Next.js?







      javascript reactjs next.js






      share|improve this question













      share|improve this question











      share|improve this question




      share|improve this question










      asked Oct 16 '17 at 12:29









      Rafal PastuszakRafal Pastuszak

      2,20222229




      2,20222229
























          2 Answers
          2






          active

          oldest

          votes


















          1














          The solution I've been happy with so far involves wrapping next/router listener methods in Observables and creating a HLA attaching router events to components on componentDidMount.



          An example implementation using RxJS:



          // I'm using recompose for and rxjs, but you should be able to modify this code easily


          // 1. sharedRouter.js
          import Router from 'next/router'
          import { Observable } from 'rxjs'


          export const routeChangeStart$ = Observable.create(
          obs => {
          console.log('route: start')
          Router.onRouteChangeStart = url => {
          obs.next(url)
          }
          }
          ).share() // note the .share() operator,
          // it ensures that we don't reassign Router.onRouteChangeStart
          // every time a new component subscribes to this observable

          export const routeChangeComplete$ = Observable.create(
          obs => {
          Router.onRouteChangeComplete = () => {
          console.log('route: complete')
          obs.next()
          }
          }
          ).share()

          export const routeChangeError$ = Observable.create(
          obs => {
          Router.onRouteChangeError = () => {
          console.log('route: error')
          obs.next()
          }
          }
          ).share()

          // 2. appBar/withRouterEvents.js
          // This one is attached to our AppNav component
          import { lifecycle } from 'recompose'
          import * as SharedRouter from './sharedRouter'

          const withRouterEvents = lifecycle({
          componentDidMount(){
          const onStartLoadingSub = Router.routeChangeStart$
          .subscribe(
          () => {
          // hide nav
          // show loading indicator
          }
          )

          const onFinishLoadingSub = Router
          .routeChangeError$
          .merge(Router.routeChangeComplete$)
          .subscribe(
          () => {
          // hide loading indicator
          }
          )

          this.subs = [
          onStartLoadingSub,
          onFinishLoadingSub
          ]
          },

          componentWillUnmount(){
          if(!Array.isArray(this.subs)) return;
          this.subs.forEach(
          sub => sub.unsubscribe()
          )
          }

          })


          // 3. appBar/index.js
          export default ({
          isNavVisible,
          isLoading,
          children
          }) => <nav className={
          isNavVisible ? 'app-bar' : 'app-bar app-bar--hidden'
          }>
          <LoadingIndicator isActive={isLoading} />
          {children}
          </nav>





          share|improve this answer































            1














            Good news! The new canary Version of next.js 6.1.1-canary.2 will support multible router event listeners.



            Install



            $ npm i next@canary


            Example



            Router.events.on('routeChangeStart', (url) => {
            console.log('App is changing to: ', url)
            })


            Docs




            • Pull Request

            • Documentation






            share|improve this answer


























            • How would you provide a listener attached to a specific route change, as in: After this specific route transition completes, do something?

              – Avremel Kaminetzky
              Jan 3 at 15:31






            • 1





              Router.events.on('routeChangeStart', (url) => { if (url === 'myURL') { // Do something } })

              – HaNdTriX
              Jan 3 at 15:40











            • How would that work for disabling the subscription? Say I have a global subscription for all route changes, and I want only turn off a single subscription.

              – Avremel Kaminetzky
              Jan 3 at 15:44






            • 1





              Oh don't use router events for those tasks! Use componentDidMount/componentWillUnmount for subscriptions.

              – HaNdTriX
              Jan 3 at 15:51






            • 1





              gist.github.com/HaNdTriX/67dbcdafbffb950804225645de312870

              – HaNdTriX
              Jan 3 at 15:54












            Your Answer






            StackExchange.ifUsing("editor", function () {
            StackExchange.using("externalEditor", function () {
            StackExchange.using("snippets", function () {
            StackExchange.snippets.init();
            });
            });
            }, "code-snippets");

            StackExchange.ready(function() {
            var channelOptions = {
            tags: "".split(" "),
            id: "1"
            };
            initTagRenderer("".split(" "), "".split(" "), channelOptions);

            StackExchange.using("externalEditor", function() {
            // Have to fire editor after snippets, if snippets enabled
            if (StackExchange.settings.snippets.snippetsEnabled) {
            StackExchange.using("snippets", function() {
            createEditor();
            });
            }
            else {
            createEditor();
            }
            });

            function createEditor() {
            StackExchange.prepareEditor({
            heartbeatType: 'answer',
            autoActivateHeartbeat: false,
            convertImagesToLinks: true,
            noModals: true,
            showLowRepImageUploadWarning: true,
            reputationToPostImages: 10,
            bindNavPrevention: true,
            postfix: "",
            imageUploader: {
            brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
            contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
            allowUrls: true
            },
            onDemand: true,
            discardSelector: ".discard-answer"
            ,immediatelyShowMarkdownHelp:true
            });


            }
            });














            draft saved

            draft discarded


















            StackExchange.ready(
            function () {
            StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f46770364%2fmultiple-event-listeners-in-next-js-router%23new-answer', 'question_page');
            }
            );

            Post as a guest















            Required, but never shown

























            2 Answers
            2






            active

            oldest

            votes








            2 Answers
            2






            active

            oldest

            votes









            active

            oldest

            votes






            active

            oldest

            votes









            1














            The solution I've been happy with so far involves wrapping next/router listener methods in Observables and creating a HLA attaching router events to components on componentDidMount.



            An example implementation using RxJS:



            // I'm using recompose for and rxjs, but you should be able to modify this code easily


            // 1. sharedRouter.js
            import Router from 'next/router'
            import { Observable } from 'rxjs'


            export const routeChangeStart$ = Observable.create(
            obs => {
            console.log('route: start')
            Router.onRouteChangeStart = url => {
            obs.next(url)
            }
            }
            ).share() // note the .share() operator,
            // it ensures that we don't reassign Router.onRouteChangeStart
            // every time a new component subscribes to this observable

            export const routeChangeComplete$ = Observable.create(
            obs => {
            Router.onRouteChangeComplete = () => {
            console.log('route: complete')
            obs.next()
            }
            }
            ).share()

            export const routeChangeError$ = Observable.create(
            obs => {
            Router.onRouteChangeError = () => {
            console.log('route: error')
            obs.next()
            }
            }
            ).share()

            // 2. appBar/withRouterEvents.js
            // This one is attached to our AppNav component
            import { lifecycle } from 'recompose'
            import * as SharedRouter from './sharedRouter'

            const withRouterEvents = lifecycle({
            componentDidMount(){
            const onStartLoadingSub = Router.routeChangeStart$
            .subscribe(
            () => {
            // hide nav
            // show loading indicator
            }
            )

            const onFinishLoadingSub = Router
            .routeChangeError$
            .merge(Router.routeChangeComplete$)
            .subscribe(
            () => {
            // hide loading indicator
            }
            )

            this.subs = [
            onStartLoadingSub,
            onFinishLoadingSub
            ]
            },

            componentWillUnmount(){
            if(!Array.isArray(this.subs)) return;
            this.subs.forEach(
            sub => sub.unsubscribe()
            )
            }

            })


            // 3. appBar/index.js
            export default ({
            isNavVisible,
            isLoading,
            children
            }) => <nav className={
            isNavVisible ? 'app-bar' : 'app-bar app-bar--hidden'
            }>
            <LoadingIndicator isActive={isLoading} />
            {children}
            </nav>





            share|improve this answer




























              1














              The solution I've been happy with so far involves wrapping next/router listener methods in Observables and creating a HLA attaching router events to components on componentDidMount.



              An example implementation using RxJS:



              // I'm using recompose for and rxjs, but you should be able to modify this code easily


              // 1. sharedRouter.js
              import Router from 'next/router'
              import { Observable } from 'rxjs'


              export const routeChangeStart$ = Observable.create(
              obs => {
              console.log('route: start')
              Router.onRouteChangeStart = url => {
              obs.next(url)
              }
              }
              ).share() // note the .share() operator,
              // it ensures that we don't reassign Router.onRouteChangeStart
              // every time a new component subscribes to this observable

              export const routeChangeComplete$ = Observable.create(
              obs => {
              Router.onRouteChangeComplete = () => {
              console.log('route: complete')
              obs.next()
              }
              }
              ).share()

              export const routeChangeError$ = Observable.create(
              obs => {
              Router.onRouteChangeError = () => {
              console.log('route: error')
              obs.next()
              }
              }
              ).share()

              // 2. appBar/withRouterEvents.js
              // This one is attached to our AppNav component
              import { lifecycle } from 'recompose'
              import * as SharedRouter from './sharedRouter'

              const withRouterEvents = lifecycle({
              componentDidMount(){
              const onStartLoadingSub = Router.routeChangeStart$
              .subscribe(
              () => {
              // hide nav
              // show loading indicator
              }
              )

              const onFinishLoadingSub = Router
              .routeChangeError$
              .merge(Router.routeChangeComplete$)
              .subscribe(
              () => {
              // hide loading indicator
              }
              )

              this.subs = [
              onStartLoadingSub,
              onFinishLoadingSub
              ]
              },

              componentWillUnmount(){
              if(!Array.isArray(this.subs)) return;
              this.subs.forEach(
              sub => sub.unsubscribe()
              )
              }

              })


              // 3. appBar/index.js
              export default ({
              isNavVisible,
              isLoading,
              children
              }) => <nav className={
              isNavVisible ? 'app-bar' : 'app-bar app-bar--hidden'
              }>
              <LoadingIndicator isActive={isLoading} />
              {children}
              </nav>





              share|improve this answer


























                1












                1








                1







                The solution I've been happy with so far involves wrapping next/router listener methods in Observables and creating a HLA attaching router events to components on componentDidMount.



                An example implementation using RxJS:



                // I'm using recompose for and rxjs, but you should be able to modify this code easily


                // 1. sharedRouter.js
                import Router from 'next/router'
                import { Observable } from 'rxjs'


                export const routeChangeStart$ = Observable.create(
                obs => {
                console.log('route: start')
                Router.onRouteChangeStart = url => {
                obs.next(url)
                }
                }
                ).share() // note the .share() operator,
                // it ensures that we don't reassign Router.onRouteChangeStart
                // every time a new component subscribes to this observable

                export const routeChangeComplete$ = Observable.create(
                obs => {
                Router.onRouteChangeComplete = () => {
                console.log('route: complete')
                obs.next()
                }
                }
                ).share()

                export const routeChangeError$ = Observable.create(
                obs => {
                Router.onRouteChangeError = () => {
                console.log('route: error')
                obs.next()
                }
                }
                ).share()

                // 2. appBar/withRouterEvents.js
                // This one is attached to our AppNav component
                import { lifecycle } from 'recompose'
                import * as SharedRouter from './sharedRouter'

                const withRouterEvents = lifecycle({
                componentDidMount(){
                const onStartLoadingSub = Router.routeChangeStart$
                .subscribe(
                () => {
                // hide nav
                // show loading indicator
                }
                )

                const onFinishLoadingSub = Router
                .routeChangeError$
                .merge(Router.routeChangeComplete$)
                .subscribe(
                () => {
                // hide loading indicator
                }
                )

                this.subs = [
                onStartLoadingSub,
                onFinishLoadingSub
                ]
                },

                componentWillUnmount(){
                if(!Array.isArray(this.subs)) return;
                this.subs.forEach(
                sub => sub.unsubscribe()
                )
                }

                })


                // 3. appBar/index.js
                export default ({
                isNavVisible,
                isLoading,
                children
                }) => <nav className={
                isNavVisible ? 'app-bar' : 'app-bar app-bar--hidden'
                }>
                <LoadingIndicator isActive={isLoading} />
                {children}
                </nav>





                share|improve this answer













                The solution I've been happy with so far involves wrapping next/router listener methods in Observables and creating a HLA attaching router events to components on componentDidMount.



                An example implementation using RxJS:



                // I'm using recompose for and rxjs, but you should be able to modify this code easily


                // 1. sharedRouter.js
                import Router from 'next/router'
                import { Observable } from 'rxjs'


                export const routeChangeStart$ = Observable.create(
                obs => {
                console.log('route: start')
                Router.onRouteChangeStart = url => {
                obs.next(url)
                }
                }
                ).share() // note the .share() operator,
                // it ensures that we don't reassign Router.onRouteChangeStart
                // every time a new component subscribes to this observable

                export const routeChangeComplete$ = Observable.create(
                obs => {
                Router.onRouteChangeComplete = () => {
                console.log('route: complete')
                obs.next()
                }
                }
                ).share()

                export const routeChangeError$ = Observable.create(
                obs => {
                Router.onRouteChangeError = () => {
                console.log('route: error')
                obs.next()
                }
                }
                ).share()

                // 2. appBar/withRouterEvents.js
                // This one is attached to our AppNav component
                import { lifecycle } from 'recompose'
                import * as SharedRouter from './sharedRouter'

                const withRouterEvents = lifecycle({
                componentDidMount(){
                const onStartLoadingSub = Router.routeChangeStart$
                .subscribe(
                () => {
                // hide nav
                // show loading indicator
                }
                )

                const onFinishLoadingSub = Router
                .routeChangeError$
                .merge(Router.routeChangeComplete$)
                .subscribe(
                () => {
                // hide loading indicator
                }
                )

                this.subs = [
                onStartLoadingSub,
                onFinishLoadingSub
                ]
                },

                componentWillUnmount(){
                if(!Array.isArray(this.subs)) return;
                this.subs.forEach(
                sub => sub.unsubscribe()
                )
                }

                })


                // 3. appBar/index.js
                export default ({
                isNavVisible,
                isLoading,
                children
                }) => <nav className={
                isNavVisible ? 'app-bar' : 'app-bar app-bar--hidden'
                }>
                <LoadingIndicator isActive={isLoading} />
                {children}
                </nav>






                share|improve this answer












                share|improve this answer



                share|improve this answer










                answered Oct 16 '17 at 12:29









                Rafal PastuszakRafal Pastuszak

                2,20222229




                2,20222229

























                    1














                    Good news! The new canary Version of next.js 6.1.1-canary.2 will support multible router event listeners.



                    Install



                    $ npm i next@canary


                    Example



                    Router.events.on('routeChangeStart', (url) => {
                    console.log('App is changing to: ', url)
                    })


                    Docs




                    • Pull Request

                    • Documentation






                    share|improve this answer


























                    • How would you provide a listener attached to a specific route change, as in: After this specific route transition completes, do something?

                      – Avremel Kaminetzky
                      Jan 3 at 15:31






                    • 1





                      Router.events.on('routeChangeStart', (url) => { if (url === 'myURL') { // Do something } })

                      – HaNdTriX
                      Jan 3 at 15:40











                    • How would that work for disabling the subscription? Say I have a global subscription for all route changes, and I want only turn off a single subscription.

                      – Avremel Kaminetzky
                      Jan 3 at 15:44






                    • 1





                      Oh don't use router events for those tasks! Use componentDidMount/componentWillUnmount for subscriptions.

                      – HaNdTriX
                      Jan 3 at 15:51






                    • 1





                      gist.github.com/HaNdTriX/67dbcdafbffb950804225645de312870

                      – HaNdTriX
                      Jan 3 at 15:54
















                    1














                    Good news! The new canary Version of next.js 6.1.1-canary.2 will support multible router event listeners.



                    Install



                    $ npm i next@canary


                    Example



                    Router.events.on('routeChangeStart', (url) => {
                    console.log('App is changing to: ', url)
                    })


                    Docs




                    • Pull Request

                    • Documentation






                    share|improve this answer


























                    • How would you provide a listener attached to a specific route change, as in: After this specific route transition completes, do something?

                      – Avremel Kaminetzky
                      Jan 3 at 15:31






                    • 1





                      Router.events.on('routeChangeStart', (url) => { if (url === 'myURL') { // Do something } })

                      – HaNdTriX
                      Jan 3 at 15:40











                    • How would that work for disabling the subscription? Say I have a global subscription for all route changes, and I want only turn off a single subscription.

                      – Avremel Kaminetzky
                      Jan 3 at 15:44






                    • 1





                      Oh don't use router events for those tasks! Use componentDidMount/componentWillUnmount for subscriptions.

                      – HaNdTriX
                      Jan 3 at 15:51






                    • 1





                      gist.github.com/HaNdTriX/67dbcdafbffb950804225645de312870

                      – HaNdTriX
                      Jan 3 at 15:54














                    1












                    1








                    1







                    Good news! The new canary Version of next.js 6.1.1-canary.2 will support multible router event listeners.



                    Install



                    $ npm i next@canary


                    Example



                    Router.events.on('routeChangeStart', (url) => {
                    console.log('App is changing to: ', url)
                    })


                    Docs




                    • Pull Request

                    • Documentation






                    share|improve this answer















                    Good news! The new canary Version of next.js 6.1.1-canary.2 will support multible router event listeners.



                    Install



                    $ npm i next@canary


                    Example



                    Router.events.on('routeChangeStart', (url) => {
                    console.log('App is changing to: ', url)
                    })


                    Docs




                    • Pull Request

                    • Documentation







                    share|improve this answer














                    share|improve this answer



                    share|improve this answer








                    edited Jan 3 at 15:40

























                    answered Jul 22 '18 at 23:44









                    HaNdTriXHaNdTriX

                    19.1k65870




                    19.1k65870













                    • How would you provide a listener attached to a specific route change, as in: After this specific route transition completes, do something?

                      – Avremel Kaminetzky
                      Jan 3 at 15:31






                    • 1





                      Router.events.on('routeChangeStart', (url) => { if (url === 'myURL') { // Do something } })

                      – HaNdTriX
                      Jan 3 at 15:40











                    • How would that work for disabling the subscription? Say I have a global subscription for all route changes, and I want only turn off a single subscription.

                      – Avremel Kaminetzky
                      Jan 3 at 15:44






                    • 1





                      Oh don't use router events for those tasks! Use componentDidMount/componentWillUnmount for subscriptions.

                      – HaNdTriX
                      Jan 3 at 15:51






                    • 1





                      gist.github.com/HaNdTriX/67dbcdafbffb950804225645de312870

                      – HaNdTriX
                      Jan 3 at 15:54



















                    • How would you provide a listener attached to a specific route change, as in: After this specific route transition completes, do something?

                      – Avremel Kaminetzky
                      Jan 3 at 15:31






                    • 1





                      Router.events.on('routeChangeStart', (url) => { if (url === 'myURL') { // Do something } })

                      – HaNdTriX
                      Jan 3 at 15:40











                    • How would that work for disabling the subscription? Say I have a global subscription for all route changes, and I want only turn off a single subscription.

                      – Avremel Kaminetzky
                      Jan 3 at 15:44






                    • 1





                      Oh don't use router events for those tasks! Use componentDidMount/componentWillUnmount for subscriptions.

                      – HaNdTriX
                      Jan 3 at 15:51






                    • 1





                      gist.github.com/HaNdTriX/67dbcdafbffb950804225645de312870

                      – HaNdTriX
                      Jan 3 at 15:54

















                    How would you provide a listener attached to a specific route change, as in: After this specific route transition completes, do something?

                    – Avremel Kaminetzky
                    Jan 3 at 15:31





                    How would you provide a listener attached to a specific route change, as in: After this specific route transition completes, do something?

                    – Avremel Kaminetzky
                    Jan 3 at 15:31




                    1




                    1





                    Router.events.on('routeChangeStart', (url) => { if (url === 'myURL') { // Do something } })

                    – HaNdTriX
                    Jan 3 at 15:40





                    Router.events.on('routeChangeStart', (url) => { if (url === 'myURL') { // Do something } })

                    – HaNdTriX
                    Jan 3 at 15:40













                    How would that work for disabling the subscription? Say I have a global subscription for all route changes, and I want only turn off a single subscription.

                    – Avremel Kaminetzky
                    Jan 3 at 15:44





                    How would that work for disabling the subscription? Say I have a global subscription for all route changes, and I want only turn off a single subscription.

                    – Avremel Kaminetzky
                    Jan 3 at 15:44




                    1




                    1





                    Oh don't use router events for those tasks! Use componentDidMount/componentWillUnmount for subscriptions.

                    – HaNdTriX
                    Jan 3 at 15:51





                    Oh don't use router events for those tasks! Use componentDidMount/componentWillUnmount for subscriptions.

                    – HaNdTriX
                    Jan 3 at 15:51




                    1




                    1





                    gist.github.com/HaNdTriX/67dbcdafbffb950804225645de312870

                    – HaNdTriX
                    Jan 3 at 15:54





                    gist.github.com/HaNdTriX/67dbcdafbffb950804225645de312870

                    – HaNdTriX
                    Jan 3 at 15:54


















                    draft saved

                    draft discarded




















































                    Thanks for contributing an answer to Stack Overflow!


                    • Please be sure to answer the question. Provide details and share your research!

                    But avoid



                    • Asking for help, clarification, or responding to other answers.

                    • Making statements based on opinion; back them up with references or personal experience.


                    To learn more, see our tips on writing great answers.




                    draft saved


                    draft discarded














                    StackExchange.ready(
                    function () {
                    StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f46770364%2fmultiple-event-listeners-in-next-js-router%23new-answer', 'question_page');
                    }
                    );

                    Post as a guest















                    Required, but never shown





















































                    Required, but never shown














                    Required, but never shown












                    Required, but never shown







                    Required, but never shown

































                    Required, but never shown














                    Required, but never shown












                    Required, but never shown







                    Required, but never shown







                    Popular posts from this blog

                    Mossoró

                    Can't read property showImagePicker of undefined in react native iOS

                    Pushsharp Apns notification error: 'InvalidToken'