Wednesday 13 May 2015

Scala.js and React: Building an Application for the Web

Scala.js compiles Scala code to JavaScript. I noticed on the Reactive Programming course at Coursera that Scala.js has been integrated into it to implement a basic spreadsheet on a Web page, suggesting good support from the Scala establishment. The principal developer of Scala.js is a collaborator of Martin Odersky at EPFL, Sébastien Doeraene, and he will be speaking about it next month (June 2015) at Scala Days in Amsterdam.

Before getting into the sample application, let's talk about the motivation for Scala.js. Basically, the Web continues to be a powerful platform for application development. Despite its many problems, it has features that the desktop and mobile phone cannot match. For example, the install process for a web application is negligible from a user's perspective: just load the web page for the first time and it is already there. Also, web applications can connect with each other via hyperlinks and REST calls.

Unfortunately on the Web, JVM languages have traditionally been limited to the server-side. Functionality on the client-side is dominated by JavaScript. The trouble is, from a developer's perspective, two languages mean a lot of extra complexity. You can't share code between them, and to pass objects at runtime between client and server, you end up writing serialization and validation logic in both languages. It would be preferable to implement a web application in a single language.

How is this possible, given that browsers only understand JavaScript? One possibility is to run JavaScript on the server-side, which is the approach that Node.js takes. However, JavaScript meets with many complaints: it is essentially untyped, and you can't take advantage of the solidity and scalability that the JVM has been providing on the server-side for so long. It would be better to use a language on both sides that can take advantage of the performance of JVM, the safety of typing, and also the FP principles crossing over into the mainstream during the past ten years. This is made possible though the use of transpilers which convert one source language (Scala in the case of Scala.js) to JavaScript.

One of the big challenges in Web programming is coordinating events so that elements in the view (client-side) are updated when the model (server-side) changes. For desktop apps, the Observer design pattern is often used, but on the Web, it takes a bit more work, and we often employ the help of some MVC (Model-View-Controller) Web framework. The most general term for getting changes to propagate automatically like this (as opposed to manually making calls from the view all the time) is "Reactive Programming". A particular form of Reactive Programming is Functional Reactive Programming (FRP) which is about capturing relationships in a composable way between values that change over time ("signals"). A related approach is to use a message passing system like Akka that keeps components loosely coupled. In both cases the key goals are to avoid the inefficiency of blocking operations and the hazards of mutable data, so making the overall system scalable and resilient.

I would propose that the term FWP (Functional Web Programming) be used to cover systems that bring FP and Reactive Programming to the Web: including Elm, ClojureScript/Om, and Play Iteratees. The FWP implementation I have chosen is a combination of Scala.js and React: this article describes setting up a simple application without going into depth about FRP or being a full tutorial.

While Scala.js was being developed, back in the JavaScript world frameworks were being developed that tackled the issues of Reactive Programming. One of the most popular has been React, developed by Facebook and Instagram. When it was introduced in May 2013, it surprised seasoned developers as it seemed to violate establish best practices. In JavaScript updating the browser DOM is slow, so it was common to only update the necessary parts when the backing model changed. However, in React when any component's state is changed, a complete re-render is done from the application developer's perspective. It's almost like serving a whole new page, guaranteeing that every place data is displayed it will be up-to-date. This avoids the dangers of mutating state but it seems like it would be very slow: still, it actually outperforms other frameworks like AngularJS thanks to some a clever diffing algorithm involving a "virtual DOM" that is maintained independently of the browser's actual DOM.



Another advantage of React is that developers concentrate on re-using components rather than templates where messy logic tends to accumulate. Components are typically written in JSX, a JavaScript extension language, and then translated to actual JavaScript using a preprocessor. For instance, consider the score text at the top left in the Libanius app (see screenshot). In JSX this would be written:
















var ScoreText = React.createClass({
  render: function() {
    return (
      <span className="score-text">
        Score: {this.props.scoreText}
      </span>
    );
  }
});
React.render(
  <ScoreText />,
  document.getElementById('content')
);

This is JavaScript plus some syntactic sugar. Notice how the score variable is passed in using the props for the component.

The code above is converted to normal JavaScript by running the preprocessor. On the command-line you would typically run something like this to watch your source directory and translate code to raw JavaScript in the build directory whenever anything changes:


> jsx --watch src/ build/

This is using standard React so far. However this is not the way we do it with Scala.js.

Firstly, there exists a Scala library from Haoyi Li called Scalatags, that includes Scala equivalents for HTML tags and attributes. Let's assume we have a file QuizScreen.scala in which we are writing the view. The core code may start off looking a bit like this:

@JSExport
object QuizScreen {
  @JSExport
  def main(target: html.Div) = {
    val quizData = // … Ajax call
    target.appendChild(
      span(`class` := "alignleft", "Score: " + quizData.score)
      // ... more view code here
    )
  }
} 

Notice that span is a Scala method (the Scalatags library has to be imported). Assuming you've configured SBT to use Scala.js (see below), this is converted to JavaScript in SBT by calling:


> fastOptJS 

A good thing about the Scala.js compiler is that it keeps the target JavaScript small by eliminating any code from included libraries that is not used. To stop this from happening on the entry point itself in QuizScreen, it is necessary to use the @JSExport annotation both on the object and the main method. This guarantees that main() will be callable from JavaScript. 

So now we have seen the React way and the Scala.js way. How do we combine them? A good option is to use the scalajs-react library from David Barri. Now the ScoreText component looks like this:

  
val ScoreText = ReactComponentB[String]("ScoreText")
    .render(scoreText => <.span(^.className := "alignleft", "Score: " + scoreText))
    .build

Compare this with the JSX version. The Scala code is more concise. Notice that the render() method is present. It's also possible to use other React lifecycle methods if necessary, like componentDidMount() for initializing the component.

Notice also that it uses a Scala span. A specialized version of Scalatags is used here. At first the extra symbols look intimidating, but just remember that < is used for tags and ^ is used for attributes, and they are imported like this:

import japgolly.scalajs.react.vdom.prefix_<^._

In classic JavaScript React, a component can hold state, as seen in references to this.state and the getInitialState() method, which might look like this.

  getInitialState: function() {
    return {data: []};
  }

The Scala version lets us define the state more clearly because it is a strongly typed language. For example, the state for the QuizScreen looks like this:

case class State(userToken: String, currentQuizItem: Option[QuizItemReact] = None,
    prevQuizItem: Option[QuizItemReact] = None, scoreText: String = "",
    chosen: Option[String] = None, status: String = "")

It is best to centralize the state for the screen like this and pass it down to components, rather than having each sub-component have its own separate state object. That could get out of hand!

By the way, you can compose components just as you can in classic React. The central component of the QuizScreen object is the QuizScreen component, and it contains the ScoreText component along with the various other bits and pieces. The code below shows how this is all put together.

val QuizScreen = ReactComponentB[Unit]("QuizScreen")
  .initialState(State(generateUserToken))
  .backend(new Backend(_))
  .render((_, state, backend) => state.currentQuizItem match {
    // Only show the page if there is a quiz item
    case Some(currentQuizItem: QuizItemReact) =>
      <.div(
        <.span(^.id := "header-wrapper", ScoreText(state.scoreText),
          <.span(^.className := "alignright",
            <.button(^.id := "delete-button",
              ^.onClick --> backend.removeCurrentWordAndShowNextItem(currentQuizItem),
                  "DELETE WORD"))
        ),
        QuestionArea(Question(currentQuizItem.prompt,
            currentQuizItem.responseType,
            currentQuizItem.numCorrectResponsesInARow)),
        <.span(currentQuizItem.allChoices.map { choice =>
          <.div(
            <.p(<.button(
              ^.className := "response-choice-button",
              ^.className := cssClassForChosen(choice, state.chosen,
                  currentQuizItem.correctResponse),
              ^.onClick --> backend.submitResponse(choice, currentQuizItem), choice))
          )}),
        PreviousQuizItemArea(state.prevQuizItem),
        StatusText(state.status))
    case None =>
      if (!state.quizEnded)
        <.div("Loading...")
      else
        <.div("Congratulations! Quiz complete. Score: " + state.scoreText)
  })
  .buildU

The central component (QuizScreen) contains the other components (implementations not shown) and also has access to a State and a Backend. The backend contains logic that is a bit more extended. For example, in the code above, observe that submitResponse is called above on the backend when a button is clicked by the user. The code invoked is:

class Backend(scope: BackendScope[Unit, State]) {
    def submitResponse(choice: String, curQuizItem: QuizItemReact) {
      scope.modState(_.copy(chosen = Some(choice)))
      val url = "/processUserResponse"
      val response = QuizItemAnswer.construct(scope.state.userToken, curQuizItem, choice)
      val data = upickle.write(response)

      val sleepMillis: Double = if (response.isCorrect) 200 else 1000
      Ajax.post(url, data).foreach { xhr =>
        setTimeout(sleepMillis) { updateStateFromAjaxCall(xhr.responseText, scope) }
      }
    }

    def updateStateFromAjaxCall(responseText: String, scope: BackendScope[Unit, State]): Unit = {
      val curQuizItem = scope.state.currentQuizItem
      upickle.read[DataToClient](responseText) match {
        case quizItemData: DataToClient =>
          val newQuizItem = quizItemData.quizItemReact
          // Set new quiz item and switch curQuizItem into the prevQuizItem position
          scope.setState(State(scope.state.userToken, newQuizItem, curQuizItem,
              quizItemData.scoreText))
      }
    }
    // more backend methods...
  }

submitResponse makes an Ajax POST call to the server, collects the results and updates the State object. The React framework will take care of the rest, i.e. updating the DOM to reflect the changes to State.

In making the Ajax call, the upickle library (again from Haoyi Li) is used for serialization/deserialization. This is also used on the server side of our Scala.js application. The core of the server side is a Spray server. A simple router is defined which recognizes the call to processUserResponse made above:
 
object Server extends SimpleRoutingApp {
  def main(args: Array[String]): Unit = {
    implicit val system = ActorSystem()
    lazy val config = ConfigFactory.load()
    val port = config.getInt("libanius.port")

    startServer("0.0.0.0", port = port) {
      // .. get route not shown here
      post {
        path("processUserResponse") {
          extract(_.request.entity.asString) { e =>
            complete {
              val quizItemAnswer = upickle.read[QuizItemAnswer](e)
              upickle.write(QuizService.processUserResponse(quizItemAnswer))
            }
          }
        }
      } 
    }
  }
}

The "processUserResponse" path extracts the post data using upickle then passes the call on to the QuizService singleton which contains the mid-tier business logic, and relies on the core Libanius library to run the main back-end functionality on the Quiz. I won't go into detail about this logic, but note that both for historical reasons and future portability it relies on simple files to hold quiz data rather than a database system.

Back to the Spray server: when the QuizScreen page is initially loaded, this route is used:

     get {
        pathSingleSlash {
          complete{
            HttpEntity(
              MediaTypes.`text/html`,
              QuizScreen.skeleton.render
            )
          }
        } 

The QuizScreen mentioned here is not the QuizScreen on the client-side that is described above. In fact, it is a server-side QuizScreen that makes a call to the client-side QuizScreen. Like this:

object QuizScreen {

  val skeleton =
    html(
      head(
        link(
          rel:="stylesheet",
          href:="quiz.css"
        ),
        script(src:="/app-jsdeps.js")
      ),
      body(
        script(src:="/app-fastopt.js"),        
        div(cls:="center", id:="container"),
        script("com.oranda.libanius.scalajs.QuizScreen().main()")
      )
    )
} 

Again the tags are from Scalatags. The main call is in the last script tag. Recall that on the client-side we use @JSExport to make the QuizScreen().main() available:

  
  @JSExport
  def main(): Unit = {
    QuizScreen() render document.getElementById("container")  
  } 

Also notice in the skeleton above, there are two included JavaScript libraries:
  • app-fastopt.js: In a Scala.js application, the *-fastopt.js file is the final output of the fastOptJS task, containing the JavaScript code that has been generated from your Scala code.
  • app-jsdeps.js: In a Scala.js application, the *-jsdeps.js, contains all additional JavaScript libraries: in our case, the only thing it incorporates is react-with-addons.min.js
Here are the essentials of the SBT configuration, which can be used as a starting point for other Scala.js projects, as it just uses the most basic dependencies, including Scalatags, upickle for serialization, and utest for testing.

import sbt.Keys._

name := "Libanius Scala.js front-end"

// Set the JavaScript environment to Node.js, assuming that it is installed, rather than the default Rhino 
scalaJSStage in Global := FastOptStage  

// Causes a *-jsdeps.js file to be generated, including (here) React
skip in packageJSDependencies := false

val app = crossProject.settings(
  unmanagedSourceDirectories in Compile +=
    baseDirectory.value  / "shared" / "main" / "scala",

  libraryDependencies ++= Seq(
    "com.lihaoyi" %%% "scalatags" % "0.5.1",
    "com.lihaoyi" %%% "utest" % "0.3.0",
    "com.lihaoyi" %%% "upickle" % "0.2.8"
  ),
  scalaVersion := "2.11.6",
  testFrameworks += new TestFramework("utest.runner.Framework")
).jsSettings(
  libraryDependencies ++= Seq(
    "org.scala-js" %%% "scalajs-dom" % "0.8.0",
    "com.github.japgolly.scalajs-react" %%% "core" % "0.8.3",
    "com.github.japgolly.scalajs-react" %%% "extra" % "0.8.3",
    "com.lihaoyi" %%% "scalarx" % "0.2.8"
  ),
  // React itself (react-with-addons.js can be react.js, react.min.js, react-with-addons.min.js)
  jsDependencies += "org.webjars" % "react" % "0.13.1" / "react-with-addons.js" commonJSName "React"

).jvmSettings(
  libraryDependencies ++= Seq(
    "io.spray" %% "spray-can" % "1.3.2",
    "io.spray" %% "spray-routing" % "1.3.2",
    "com.typesafe.akka" %% "akka-actor" % "2.3.6",
    "org.scalaz" %% "scalaz-core" % "7.1.2"
  )
)

lazy val appJS = app.js.settings(

  // make the libanius core JAR available
  // ...
  unmanagedBase <<= baseDirectory(_ / "../shared/lib")
)

lazy val appJVM = app.jvm.settings(

  // make sure app-fastopt.js, app-jsdeps.js, quiz.css, the libanius core JAR, application.conf 
  // and shared source code is copied to the server
  // ...
)

As you can see, the special thing about a Scala.js client-server SBT configuration is that it is divided into three parts: js, jvm, and shared. The js folder contains code to be compiled by ScalaJS, the jvm folder contains regular Scala code used on the server-side, and the shared folder contains code and configuration that should be accessible to both js and jvm. This is achieved by using the crossProject builder from Scala.js, which constructs two separate projects, the js one and the jvm one.

So far we've been assuming that any generated JavaScript will run in a browser. However, Scala.js also works with "headless runtimes" like Node.js or PhantomJS to ensure you can run it from the command-line on the server-side too: this is important in testing. Notice the scalaJSStage in Global := FastOptStage line above.

Now for a grand overview of the web application, let's look at the directory structure. You can see how slim the application really is: there are only a few key source files.

libanius-scalajs-react/  
  build.sbt
  app/
    js/
      src/
        main/
          scala/
            com.oranda.libanius.scalajs/
              QuizScreen.scala
      target/
    jvm/
      src/
        main/
          resources/
            application.conf
          scala/
            com.oranda.libanius.sprayserver/
              QuizScreen.scala
              QuizService.scala
              Server.scala
      target/
    shared/
      lib/
        libanius-0.982.jar
      src/
        main/
          resources/
            quiz.css
          scala/
            com.oranda.libanius.scalajs/
              ClientServerObjects.scala
                QuizItemReact

Again notice there is a QuizScreen on both the server-side and client-side: the former calls the latter.

One thing that I didn't mention yet is the quiz.css file that is used in the client-side QuizScreen. This is just an old-fashioned CSS file, but of course it also possible to use LESS files. Furthermore, if you don't anticipate having a graphic designer want to change your styles, you could even go the whole way in making your application type safe, and write your styles in Scala with ScalaCSS.

The full code for this front-end to Libanius is on Github. As of writing there is a deployment on Heroku (may not be supported indefinitely). For a full tutorial on Scala.js, see Hands-on Scala.js from Haoyi Li. There is also a small official tutorial.

46 comments:

  1. Great article, want to learn/use scalajs react and this helps alot. Cheers :)

    ReplyDelete
  2. Excellent tutorial buddy. Directly I saw your blog and way of teaching was perfect, Waiting for your next tutorial.
    best rpa training institute in chennai | rpa training in velachery | rpa training in chennai omr

    ReplyDelete
  3. I am really happy with your blog because your article is very unique and powerful for new reader.
    Click here:
    selenium training in chennai
    selenium training in bangalore
    selenium training in Pune
    selenium training in pune
    Selenium Online Training

    http://scottsdigitalcommunity.blogspot.com/2013/05/developing-spring-mvc-project-using.html

    ReplyDelete
  4. Excellent tutorial buddy. Directly I saw your blog and way of teaching was perfect, Waiting for your next tutorial.Requesting you to please sharing such type of information on Blueprism too beacuse many of people also searching such a great information on Blueprism too.
    Thanks and Regards,
    Blue prism training in chennai
    Best Blue prism training in chennai
    Blue prism training cost in chennai

    ReplyDelete
  5. Inspiring writings and I greatly admired what you have to say , I hope you continue to provide new ideas for us all and greetings success always for you.
    Keep update more information..
    mi service center
    redmi service center near me
    redmi mobile service centre in chennai
    redmi note service center in chennai
    redmi service center in velachery

    ReplyDelete
  6. Useful information.I am actual blessed to read this article.thanks for giving us this advantageous information.I acknowledge this post.and I would like bookmark this post.Thanks
    devops online training

    aws online training

    data science with python online training

    data science online training

    rpa online training

    ReplyDelete
  7. I think the admin of this website is really working hard for his web page,
    since here every information is quality based information. web and mobile app development company

    ReplyDelete
  8. This is excellent information. It is amazing and wonderful to visit your site...
    Event management company in chennai

    ReplyDelete
  9. Appreciating the persistence you put into your blog and detailed information you provide.

    Data science Course Training in Chennai |Best Data Science Training Institute in Chennai

    ReplyDelete
  10. I really appreciate this post and I like this very much. I am waiting for new post here and please keep it up in future.Web Designers in Bangalore | Website Design Company Bangalore | Web Design Company In Bangalore | Web Designing Company In Bangalore

    ReplyDelete
  11. Linking is very useful thing.you have really helped lots of people who visit blog and provide them use full information.Automation Anywhere Training in Bangalore

    ReplyDelete
  12. Really it was an awesome article,very interesting to read.You have provided an nice article,Thanks for sharing.blue prism training in bangalore

    ReplyDelete
  13. This is really an awesome post, thanks for it. Keep adding more information to this.openspan training in bangalore

    ReplyDelete
  14. That was really a great Article.Thanks for sharing information. Continue doing this.

    Best SAP S4 HANA Training in Bangalore for SAP, Real Time Experts Training Center provides the sap training project with trainers having more than 5 Years of sap training experience; We also provide 100% placement support.

    ReplyDelete
  15. I am happy for sharing on this blog its awesome blog I really impressed. thanks for sharing.

    Looking for Salesforce CRM Training in Bangalore, learn from eTechno Soft Solutions Salesforce CRM Training on online training and classroom training. Join today!

    ReplyDelete
  16. This is so elegant and logical and clearly explained. Brilliantly goes through what could be a complex process and makes it obvious.

    learn cyber security

    ReplyDelete
  17. Did you realize there is a 12 word phrase you can say to your crush... that will trigger deep emotions of love and instinctual attractiveness to you buried inside his chest?

    That's because hidden in these 12 words is a "secret signal" that fuels a man's impulse to love, cherish and protect you with his entire heart...

    =====> 12 Words That Trigger A Man's Love Instinct

    This impulse is so hardwired into a man's mind that it will make him work better than ever before to to be the best lover he can be.

    Matter-of-fact, triggering this mighty impulse is so mandatory to achieving the best possible relationship with your man that the moment you send your man a "Secret Signal"...

    ...You will soon find him open his soul and heart to you in such a way he haven't experienced before and he will perceive you as the one and only woman in the galaxy who has ever truly appealed to him.

    ReplyDelete

  18. This is so elegant and logical and clearly explained. Brilliantly goes through what could be a complex process and makes it obvious.
    windows azure training

    ReplyDelete

  19. This is most informative and also this post most user friendly and super navigation to all posts. Thank you so much for giving this information to me.Microsoft Azure training in Chennai.
    Java training in chennai | Java training in annanagar | Java training in omr | Java training in porur | Java training in tambaram | Java training in velachery

    ReplyDelete
  20. I believe that your blog will surely help the readers who are really in need of this vital piece of information. Waiting for your updates.
    AWS training in chennai | AWS training in annanagar | AWS training in omr | AWS training in porur | AWS training in tambaram | AWS training in velachery

    ReplyDelete
  21. vwonderful article. Very interesting to read this article.I would like to thank you for the efforts you had made for writing this awesome article.PHP Training in Chennai

    PHP Online Training in Chennai

    Machine Learning Training in Chennai

    iOT Training in Chennai

    Blockchain Training in Chennai

    Open Stack Training in Chennai

    ReplyDelete
  22. Very nice post here and thanks for it .I always like and such a super contents of these post.Excellent and very cool idea and great content of different kinds of the valuable information's.
    DevOps Training in Chennai

    DevOps Course in Chennai


    ReplyDelete
  23. Python Training in Chennai | Infycle Technologies


    If Python is a work you've always wanted, we at Infycle are here to help you make it a reality. Infycle Technologies provides Python Training in Chennai, with various levels of highly sought-after software courses such as Oracle, Java, Python, Big Data, and others, delivered through 100% hands-on practical training with industry experts. In addition, mock interviews will be conducted. For more details contact 7502633633 to grab a free demo.
    python training in Chennai

    ReplyDelete
  24. Title:

    No.1 AWS Training Institute in Chennai | Infycle Technologies


    Description:
    Study Amazon Web Services for making your career as a shining sun with Infycle Technologies. Infycle Technologies is the best AWS training institute in Chennai, providing complete hands-on practical training of professional specialists in the field. In addition to that, it also offers numerous programming language tutors in the software industry such as Oracle, Python, Big Dat, Hadoop, etc. Once after the training, interviews will be arranged for the candidates, so that, they can set their career without any struggle. Of all that, 200% placement assurance will be given here. To have the best career, call 7502633633 to Infycle Technologies and grab a free demo to know more.

    ReplyDelete
  25. Title:
    Learn Big Data Course in Chennai | Infycle Technologies

    Description:
    If Big Data is a job that you're dreaming of, then we, Infycle are with you to make your dream into reality. Infycle Technologies offers the best Big Data Course Chennai, with various levels of highly demanded software courses such as Java, Python, Hadoop, AWS, etc., in 100% hands-on practical training with specialized tutors in the field. Along with that, the pre-interviews will be given for the candidates, so that, they can face the interviews with complete knowledge. To know more, dial 7502633633 for more.

    Best training in Chennai

    ReplyDelete
  26. Infycle Technologies, the best software training institute in Chennai offers the No.1 Data Science training in Chennai for Students, tech professionals, and freshers. In addition to the Data Science Training Course, Infycle also offers other professional courses such as Cyber Security, Python, Oracle, Java, Power BI, Digital Marketing, Big Data, etc., which will be trained with 100% practical classes. After the completion of training, the trainees will be sent for placement interviews in the top MNC's. Call 7502633633 to get more info and a free demo.

    ReplyDelete
  27. Set your career goal towards Oracle for a wealthy future with Infycle. Infycle Technologies is the best Oracle training institute in Chennai, which gives the most trusted and best Oracle Training in hands-on practical training that will be guided by professional tutors in the field. In addition to this, the mock interviews will be given to the candidates, so that, they can face the interviews with full confidence. Apart from all, the candidates will be placed in the top MNC's with a great salary package. To get it all, call 7502633633 and make this happen for your happy life.
    Best software training in Chennai

    ReplyDelete
  28. Digital commerce, also known as e-commerce , is indeed a business concept that allows businesses and individuals to buy and sell goods through the Internet. The growth of online developers in India has been fueled by advancements in the IT industry and increased consumer understanding of the Internet.
    PPC company in India
    PPC Company in USA
    Social Media Marketing Agency in Delhi
    Adwords- PPC Management Company Delhi
    Website Development company in ranchi
    Creative Web Development Company

    ReplyDelete
  29. The blog was absolutely fantastic! Lot of information is helpful in some or the other way. Keep updating the blog, looking forward for more content...Great job, keep it up. Thank You for this useful information about Application Modernization Services.

    ReplyDelete
  30. Hey, what a brilliant message I have actually stumbled upon and thought me I have been locating for this comparable sort of blog post for past a week as well as barely encountered this. Thank you really much as well as I will certainly seek even more posts from you.
    Archives
    eprimefeed.com
    Latest News
    Economy
    Politics
    Tech
    Sports
    Movies
    Fashion

    ReplyDelete
  31. Great Post. Very informative. Keep Sharing!!

    Apply Now for ReactJS Training in Noida

    For more details about the course fee, duration, classes, certification, and placement call our expert at 70-70-90-50-90

    ReplyDelete