10

how can I format a date from a standard JS date object, which has come from the server and is fed into React via Redux through a REST api, which has an initial output of:

"2016-05-16T13:07:00.000Z"

Into a different format? So it is listed as DD MM YYYY?

Can I do this in the Reducer of Action by manipulating the request result, or the payload:

import { FETCH_BOOKS, FETCH_BOOK } from '../actions';

const INITIAL_STATE = { books: [], book: null };

export default function(state = INITIAL_STATE, action) {
    switch(action.type) {
        case FETCH_BOOK:
            return { ...state, book: action.payload.data };
        case FETCH_BOOKS:
            return { ...state, books: action.payload.data };
    }
    return state;
}

With action:

export function fetchBooks() {
    const request = axios.get('/api/books');

    return {
        type: FETCH_BOOKS,
        payload: request
    }
}

fetchBooks() is called in the componentWillMount() method of the Books React component:

componentWillMount() {
    this.props.fetchBooks();
}

The api returns the following structured JSON:

[{"_id":0,"date":"2016-05-16T13:07:00.000Z","title":"Eloquent JavaScript","description":"Learn JavaScript"}]

Update

Thank you very much Nicole and nrabinowitz - I have implemented the following changes... in the action creators

function transformDateFormat(json) {
    const book = {
        ...json,
        id: json.data._id,
        // date: new Date(moment(json.data.date).format('yyyy-MM-dd'))
        date: new Date(json.data.date)
    }
    console.log(book);
    return book;
}

export function fetchBook(id) {
    const request = axios.get(`/api/books/${id}`)
        .then(transformDateFormat);

    return {
        type: FETCH_BOOK,
        payload: request 
    }
};

I am logging the book object in the transform date function, and it is adding id and date to the root of the object, but the structure of the json actually needs to be book.data.id and book.data.date. I'm not sure how to create the correct structure within the transformDate function to return to the action creator.

This is the book object from the transform date function console log:

Object {data: Object, status: 200, statusText: "OK", headers: Object, config: Object…}
config: Object
data: Object
    __v:0
    _id: "5749f1e0c373602b0e000001"
    date: "2016-05-28T00:00:00.000Z"
    name:"Eloquent JavaScript"
    __proto__:Object
date: Sat May 28 2016 01:00:00 GMT+0100 (BST)
headers: Object
id: "5749f1e0c373602b0e000001"
request: XMLHttpRequest
status: 200
statusText: "OK"
__proto__: Object

My function needs to place the id and date inside the data object. (The date is just standard date format as something is wrong with my implementation of moment (date: new Date(moment(json.data.date).format('yyyy-MM-dd')) returns Invalid date).

Thanks again for all your help - really appreciated.

Le Moi
  • 975
  • 2
  • 15
  • 41
  • `new Date(moment(json.data.date).format('yyyy-MM-dd'))` should be `moment(new Date(json.data.date)).format('yyyy-MM-dd')`. Moment is pretty flexible, you should be able to drop the `new Date` part altogether. – nrabinowitz Jun 06 '16 at 17:52
  • Thanks @nrabinowitz, tried that and it is manipulating the date, but it's coming out as `yyyy-06-Tue` and Redux form still doesn't load in the date values like it is the other values. Not sure what to do with this now - well and truly stuck. – Le Moi Jun 07 '16 at 17:13
  • 1
    My question is... what happers when you have a local date of 1st of let's say July... it would be like "2020-07-01" in the date picker. The value that is saved in redux is "Wed Jul 01 2020 00:00:00 GMT+0300 (Eastern European Summer Time)", but when I look at the redux store, the value is "2020-06-30T21:00:00.000Z" That means that I am practically going to save the wrong date in the database (even if it is in utc format). How will I convert 2020-06-30T21:00:00.000Z to 2020-07-01T00:00:000Z in order to have the correct date? I am not interested in the time. – SoftDev30_15 Jul 24 '20 at 08:49

4 Answers4

9

I would do this transformation immediately when the data arrives in the frontend, i.e. in your AJAX request. This way, your frontend application only works with the data as you intend them to be shaped, and your request handler encapsulates and hides the data structures as they are provided from the server and passes the data to the frontend application in the desired shape.

(In Domain-Driven Design, this goes under the name of "Anticorruption Layer").

Regarding the technical solution:

Currently, your event payload contains a promise to the data. You don't show us where you invoke fetchBooks(), i.e. where the promise is executed. That's where you need to do the data transformation. (But personally, I'd rather keep the data transformation inside the fetchBooks function.)

Also, that's the point where you need to send the data to the redux store. To be able to reduce events asynchronously, you need something like redux-thunk.

A solution might look like this (my apologies, I'm not very experienced in using promises, so I'll use a callback here, but maybe you can somehow translate that to a promise):

export function fetchBooks(callback) {
    const request = axios.get('/api/books')
                    .then(function (response) {
                       callback(transformData(response));
                    });
}

transformData is supposed to do the date transformation from backend format to frontend format.

Somewhere else you probably trigger an action that gets emitted in order to fetch the books (let's call it loadBooks for now). That's where you need to do the asynchronous dispatch:

function loadBooks() {
    return (dispatch, getState) => {

        fetchBooks(response => {
            dispatch(receiveBooks(response));
        });
    };
}

function receiveBooks(data) {
    return {
        type: FETCH_BOOKS,
        payload: data
    };
}

This way, you will only dispatch the data to the redux store once it was actually returned from the AJAX call.

To use redux-thunk, you need to inject its middleware into your store like this:

import thunkMiddleware from "redux-thunk";
import { createStore, applyMiddleware } from "redux";

const createStoreWithMiddleware = applyMiddleware(
    thunkMiddleware
)(createStore);

and then pass your reducers function to this store.

Please keep asking if you have any further questions.

Nicole
  • 1,717
  • 1
  • 14
  • 17
  • Thanks Nicole. Technically how do I do this in Redux though? I am logging to the console payload.data within the reducer and I am getting undefined three times, then an object. Presumably because this is an ajax request and the response isn't immediate? How do I transform this when the data arrives? – Le Moi May 16 '16 at 14:14
  • Sorry for the delay, having a second child is hectic. Thanks so much for your help, I am unsure what you mean about translating the callback to a promise. Can't work out how to integrate the solution. I will keep reading up and try to work it out. Really appreciate the help :) – Le Moi Jun 03 '16 at 22:28
  • No worries, enjoy the family life :-) What I meant was: axios is an AJAX library that works with promises, so it returns a promise and you store that promise in the redux store (if I understand correctly). So at some point you will read the value out of the promise. My sketch of a solution does not work with promises, so you will have to adapt the code where you access the promise. – Nicole Jun 03 '16 at 22:47
  • Thank you :) The promise is accessed in the reducer I think, ReduxPromise is middleware that holds the promise until it returns and then passes it to the reducer. Could I do the manipulation in the reducer? – Le Moi Jun 03 '16 at 23:37
  • Just follow nrabinowitz' suggestion, he shows you the correct way to work with promises. – Nicole Jun 04 '16 at 06:31
4

I agree with @Nicole here - this is best handled in the AJAX client. One pattern I often use here is a DataMapper, something like:

function jsonToBook(json) {
    // do whatever transformations you need here
    var book = {
        ...json,
        id: json._id,
        date: new Date(json.date)
    }
    // return the transformed, app-appropriate object
    return book;
}

Then, in your AJAX client, you can put the transform into a .then call:

const request = axios.get('/api/books')
    .then(jsonToBook);

This still returns a promise, so your Promise middleware should still work as expected. One advantage of this is that the data mapper is really easy to test, but logic in the AJAX client itself is harder.

One other point here is that I'd recommend parsing to a Date object, not a formatted string. This is opinion, but in general I think that formatting concerns are the province of the view (that is, the React components) rather than the store/reducers. Passing a Date instead of a string makes it easier to format the date differently in different parts of the app, for example.

For the actual date formatting, see Moment.js or D3's time-formatting utils.

nrabinowitz
  • 55,314
  • 10
  • 149
  • 165
1

Rather than modifying the response once received, it may be better to address the issue using axios library's transformResponse function as part of the call per the [axios docs][1] and [example][2]:

const ISO_8601 = /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}.\d{3})Z/;

export function fetchBooks() {
   const request = axios.get('/api/books', {
      transformResponse: axios.defaults.transformResponse.concat(function (data, headers) {
        Object.keys(data).forEach(function (k) {
          if (ISO_8601.test(data[k])) {
            data[k] = new Date(Date.parse(data[k]));
          }
        });
        return data;
      })
    });

  return {
    type: FETCH_BOOKS,
    payload: request
  }
}

If you want the dates to be consistently returned as objects throughout your application then an interceptor. Note that the following example will only update the top-level objects from strings to dates rather than nested objects:

axios.interceptors.response.use(function (response) {
  // Identify data fields returning from server as ISO_8601 formatted dates
  var ISO_8601 = /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}.\d{3})Z/;

  var data = response.data
  Object.keys(data).forEach(function (k) {
    if (ISO_8601.test(data[k])) {
      data[k] = new Date(Date.parse(data[k]));
    }
  });
  return response;
}, function (error) {
  // Do something with response error
  return Promise.reject(error);
});
benmac
  • 103
  • 7
1

serify-deserify reversibly transforms unserializable values into serializable ones, and back. It natively supports BigInt, Date, Map, and Set, and you can easily add support for custom types.

The package includes a Redux middleware that applies the serify transformation on the way into the store. Wrap retrieval functions in the deserify function in order to transform the results to the appropriate type.

Full disclosure: I am the package creator!