A dynamic string array that's loopable without requiring a count
review if this code can be improved upon for any redundancy or optimizations.


Micro optimization

String duplication does not take advantage of the pre-calculation of the length.

    data[i] = malloc(sizeof(char) * strlen(*items) + 1);
    if (data[i] == NULL) goto err;
    strcpy(data[i], *items);


For longer strings, code may (or may not) improve with the below.  This tended to improve things.  Less so today (usually a tie) unless in embedded platforms.

    size_t sz = strlen(*items) + 1;
    data[i] = malloc(sz);  // dealing with `char` strings, no need for sizeof(char)
    if (data[i] == NULL) goto err;
    memcpy(data[i], *items, sz);


Note: sizeof(char) * strlen(*items) + 1 is logically wrong.  Should have been sizeof(char) * (strlen(*items) + 1), yet since sizeof(char) is 1, makes no arithmetic difference.

Redundancy

No need to start at 0 and add 1: Start at 1.  Better to use size_t for sizing.  Better to size to the referenced type, than explicitly code the type.

// int size = 0; 
size_t size = 1; 
for (char **ptr = items; *ptr; ptr++) {
    size++;
}
// char **data = malloc(sizeof(char *) * (size + 1));
char **data = malloc(sizeof *data * size);


Even better, just subtract:

for (char **ptr = items; *ptr; ptr++) {
    ;
}
size_t size = ptr - items; 
char **data = malloc(sizeof *data * size);




Design flaw

if (data[i] == NULL) goto err; fails to free prior allocations.

Design

The for (int i = 0; *items; items++, i++) increments one pointer with a ++ and indexes the other.  I'd expect the same for both.

Caller flaw

free(data); leaks the individual strings. Recommend coding a freeList(char **).